[
  {
    "path": ".envrc",
    "content": "use flake\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"cargo\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n    open-pull-requests-limit: 10\n"
  },
  {
    "path": ".gitignore",
    "content": "*.qcow2\nvar/*.xml\nvar/*.1\nconfig.dhall\nresult\n.direnv\n\n# Added by cargo\n\n/target\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"waifud\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [ \"Xe Iaso <me@xeiaso.net>\" ]\nbuild = \"src/build.rs\"\nrepository = \"https://github.com/Xe/waifud\"\nlicense = \"mit\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[profile.release]\nlto = true\n\n[dependencies]\nanyhow = \"1\"\nasync-trait = \"0.1.68\"\naxum = \"0.6\"\naxum-client-ip = \"0.3\"\naxum-macros = \"0.3\"\naxum-extra = { version = \"0.5\", features = [\"spa\"] }\nbb8 = \"0.7\"\nchrono = \"0.4\"\nclap = { version = \"4\", features = [\"derive\"] }\nclap_mangen = \"0.2\"\nclap_complete = \"4\"\ndirs = \"4\"\nedit = \"0.1\"\nfailure = \"0.1\"\nfutures = \"0.3\"\nhex = { version = \"0.4\", features = [ \"serde\" ] }\nhyper = \"0.14\"\nhyper-tls = \"0.5\"\nmac_address = \"1\"\nnames = \"0.14\"\nrand = \"0.8\"\nrusqlite_migration = \"1.0\"\nscraper = \"0.14.0\"\nserde_dhall = \"0.12\"\nserde_json = \"1\"\nserde_yaml = \"0.9\"\ntabular = \"0.2\"\nthiserror = \"1\"\ntracing = \"0.1\"\ntracing-futures = \"0.2\"\ntracing-log = \"0.1\"\ntracing-subscriber = \"0.3\"\nurl = \"2\"\n\nbb8-rusqlite = { git = \"https://github.com/pleshevskiy/bb8-rusqlite\", branch = \"bump-rusqlite\" }\nmaud = { git = \"https://github.com/Xe/maud\", rev = \"a40596c42c7603cc4610bbeddea04c4bd8b312d9\", features = [\"axum-core\", \"axum\"] }\nvirt = \"0.3\"\nvirt-sys = \"0.2\"\n\nrotbart = { path = \"./lib/rotbart\" }\ntailscale_client = { path = \"./lib/tailscale_client\" }\nts_localapi = { path = \"./lib/ts_localapi\" }\n\n[dependencies.rusqlite]\nversion = \"0.26\"\nfeatures = [ \"bundled\", \"uuid\", \"serde_json\", \"chrono\" ]\n\n[dependencies.serde]\nversion = \"1\"\nfeatures = [ \"derive\" ]\n\n[dependencies.reqwest]\nversion = \"0.11\"\nfeatures = [ \"json\" ]\n\n[dependencies.tokio]\nversion = \"1\"\nfeatures = [ \"full\" ]\n\n[dependencies.tower]\nversion = \"0.4\"\nfeatures = [ \"full\" ]\n\n[dependencies.tower-http]\nversion = \"0.4\"\nfeatures = [ \"full\" ]\n\n[dependencies.uuid]\nversion = \"0.8\"\nfeatures = [ \"serde\", \"v4\" ]\n\n[build-dependencies]\nructe = { version = \"0.15\" }\n\n[dev-dependencies]\nructe = { version = \"0.15\" }\n\n[workspace]\nmembers = [ \"lib/*\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2022 Xe Iaso <me@xeiaso.net>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# waifud\n\n![enbyware](https://pride-badges.pony.workers.dev/static/v1?label=enbyware&labelColor=%23555&stripeWidth=8&stripeColors=FCF434%2CFFFFFF%2C9C59D1%2C2C2C2C)\n![made with Nix](https://img.shields.io/badge/made%20with-Nix-blue?logo=nixos)\n![built with Garnix](https://img.shields.io/static/v1?label=Built%20with&message=Garnix&color=blue&style=flat&logo=nixos&link=https://garnix.io&labelColor=111212)\n![license](https://img.shields.io/github/license/Xe/waifud)\n![language count](https://img.shields.io/github/languages/count/Xe/waifud)\n![repo size](https://img.shields.io/github/repo-size/Xe/waifud)\n\nA few tools to help me manage and run virtual machines across a homelab cluster.\n\nwaifud was made for my own personal use and I do not expect it to be very useful\noutside that context. If you do want to run this on your\ninfrastructure anyways, please [contact me](https://xeiaso.net/contact).\n\n<big>THIS IS EXPERIMENTAL! USE IT AT YOUR OWN PERIL!</big>\n\nTODO(Xe): Link to blogpost on the design/implementation once it is a thing.\n\nBlogposts about waifud:\n - [waifud Plans](https://xeiaso.net/blog/waifud-plans-2021-06-19)\n - [waifud Progress Report #1](https://xeiaso.net/blog/waifud-progress-2022-02-06)\n - [waifud Progress Report #2](https://xeiaso.net/blog/waifud-progress-report-2)\n\nOverall architecture diagram (with incomplete components marked with a\nclock):\n\n```mermaid\nflowchart TD\n    subgraph control plane\n    WD[fa:fa-rust waifud]\n    WC[fa:fa-rust waifuctl]\n    ID[fa:fa-golang fa:fa-clock isekaid]\n    MD[fa:fa-golang fa:fa-clock megamid]\n    PD[fa:fa-golang fa:fa-clock portald]\n    end\n    subgraph VM plane\n    LV[fa:fa-c libvirt]\n    WH[fa:fa-linux runner\\nnodes]\n    VM[fa:fa-linux virtual\\nmachines]\n    end\n    subgraph external\n    TS[fa:fa-golang Tailscale]\n    end\n\n    PD --> |tailnet ingress for| WD\n    WC --> |operator tool for| WD\n    WC --> |usually connects via|PD\n    ID --> |fetches node metadata\\nand secrets for| WD\n    VM --> |cloud-init\\nmetadata| ID\n    WD --> |manages libvirt on| WH\n    LV --> |actually runs VMs| VM\n    VM --> |network storage| MD\n    WD --> |sets limits for\\nrequests metrics from| MD  \n    WH --> |runs| LV\n    WH <--> |subnet router\\ninterconnect| TS\n    TS --> |network layer for| PD\n    VM --> |usually a part of| TS\n```\n"
  },
  {
    "path": "config.example.dhall",
    "content": "let Tailscale =\n      { Type = { apiKey : Text, tailnet : Text }\n      , default =\n        { apiKey = env:TAILSCALE_API_KEY ? \"\"\n        , tailnet = env:TAILSCALE_TAILNET ? \"cetacean.org.github\"\n        }\n      }\n\nlet Config =\n      { Type =\n          { baseURL : Text\n          , hosts : List Text\n          , bindHost : Text\n          , port : Natural\n          , rpoolBase : Text\n          , qemuPath : Text\n          , tailscale : Tailscale.Type\n          }\n      , default =\n        { baseURL = \"http://100.100.100.100:23818\"\n        , hosts = [ \"vmhost1\", \"vmhost2\" ]\n        , bindHost = \"::\"\n        , port = 23818\n        , rpoolBase = \"rpool/local/vms\"\n        , qemuPath = \"/run/libvirt/nix-emulators/qemu-system-x86_64\"\n        , tailscale = Tailscale::{=}\n        }\n      }\n\nlet defaultPort = env:PORT ? 23818\n\nin  Config::{ port = defaultPort }\n"
  },
  {
    "path": "default.nix",
    "content": "(import (fetchTarball {\n  url =\n    \"https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz\";\n  sha256 = \"0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2\";\n}) { src = ./.; }).defaultNix\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  inputs = {\n    naersk.url = \"github:nmattia/naersk/master\";\n    naersk.inputs.nixpkgs.follows = \"nixpkgs\";\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n    utils.url = \"github:numtide/flake-utils\";\n\n    xess = {\n      url = \"github:Xe/Xess\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n      inputs.utils.follows = \"utils\";\n    };\n\n    deno2nix = {\n      url = \"github:Xe/deno2nix\";\n      inputs.nixpkgs.follows = \"nixpkgs\";\n      inputs.flake-utils.follows = \"utils\";\n    };\n  };\n\n  outputs = { self, nixpkgs, utils, naersk, xess, deno2nix, ... }@inputs:\n    utils.lib.eachDefaultSystem (system:\n      let\n        pkgs = import nixpkgs {\n          inherit system;\n          overlays = [ deno2nix.overlays.default ];\n        };\n        naersk-lib = pkgs.callPackage naersk { };\n      in rec {\n        packages = rec {\n          unique-monster = pkgs.stdenv.mkDerivation {\n            src = self.packages.\"${system}\".waifud;\n            pname = \"unique-monster\";\n            version = self.packages.\"${system}\".waifud-bin.version;\n            phases = \"installPhase\";\n            installPhase = ''\n              mkdir -p $out/bin\n              cp $src/bin/unique-monster $out/bin\n            '';\n          };\n\n          waifud-bin = naersk-lib.buildPackage {\n            pname = \"waifud-bin\";\n            src = ./.;\n            buildInputs = with pkgs; [\n              pkg-config\n              openssl\n              sqlite-interactive\n              libvirt\n            ];\n          };\n\n          waifud-frontend = let\n            build = { entrypoint, name ? entrypoint, minify ? true }:\n              pkgs.deno2nix.mkBundled {\n                pname = \"xesite-frontend-${name}\";\n                inherit (waifud-bin) version;\n\n                src = ./frontend;\n                lockfile = ./frontend/deno.lock;\n\n                output = \"${entrypoint}.js\";\n                outPath = \"static/js\";\n                entrypoint = \"./${entrypoint}.tsx\";\n                importMap = \"./import_map.json\";\n                inherit minify;\n              };\n\n            instance_detail = build { entrypoint = \"instance_detail\"; };\n            instance_create = build { entrypoint = \"instance_create\"; };\n          in pkgs.symlinkJoin {\n            name = \"waifud-frontend-${waifud-bin.version}\";\n            paths = [ instance_detail instance_create ];\n          };\n\n          waifud = pkgs.symlinkJoin {\n            name = \"waifud-${waifud-bin.version}\";\n            paths = with self.packages.\"${system}\"; [\n              waifud-bin\n              waifud-frontend\n            ];\n          };\n\n          waifuctl = pkgs.stdenv.mkDerivation {\n            src = self.packages.\"${system}\".waifud;\n            pname = \"waifuctl\";\n            version = self.packages.\"${system}\".waifud-bin.version;\n            phases = \"installPhase\";\n            installPhase = ''\n              mkdir -p $out/bin\n              cp $src/bin/waifuctl $out/bin\n              mkdir -p $out/share/man/man1\n              HOME=. $out/bin/waifuctl utils manpage $out/share/man/man1\n              gzip -r $out/share/man/man1\n            '';\n          };\n        };\n\n        defaultPackage = self.packages.\"${system}\".waifuctl;\n\n        apps = {\n          unique-monster =\n            utils.lib.mkApp { drv = self.packages.\"${system}\".unique-monster; };\n          waifud = utils.lib.mkApp { drv = self.packages.\"${system}\".waifud; };\n          waifuctl =\n            utils.lib.mkApp { drv = self.packages.\"${system}\".waifuctl; };\n        };\n\n        defaultApp = self.apps.\"${system}\".waifuctl;\n\n        nixosModules = {\n          waifuctl = { ... }: {\n            environment.defaultPackages =\n              [ self.packages.\"${system}\".waifuctl ];\n          };\n\n          waifud-common = { lib, ... }: {\n            users.groups.waifud = lib.mkDefault { };\n\n            users.users.waifud = {\n              createHome = true;\n              description = \"waifud user\";\n              isSystemUser = true;\n              group = \"waifud\";\n              home = \"/var/lib/waifud\";\n            };\n          };\n\n          waifud-host = { lib, pkgs, config, ... }:\n            with lib;\n            let cfg = config.xeserv.waifud;\n            in {\n              imports = [\n                self.nixosModules.\"${system}\".waifud-common\n                self.nixosModules.\"${system}\".waifuctl\n              ];\n\n              config = {\n                systemd.services = {\n                  waifud = {\n                    wantedBy = [ \"multi-user.target\" ];\n\n                    environment = {\n                      RUST_LOG = \"tower_http=debug,waifud=debug,info\";\n                    };\n                    serviceConfig = {\n                      User = \"waifud\";\n                      Group = \"waifud\";\n                      Restart = \"always\";\n                      WorkingDirectory = \"${self.packages.\"${system}\".waifud}\";\n                      RestartSec = \"30s\";\n                      ExecStart = \"${waifud}/bin/waifud --config ${cfgDhall}\";\n                    };\n                  };\n                };\n              };\n            };\n\n          waifud-runner = { pkgs, lib, config, ... }:\n            with lib;\n            let cfg = config.xeserv.waifud.runner;\n            in {\n              imports = [ self.nixosModules.\"${system}\".waifud-common ];\n\n              options.xeserv.waifud.runner = with lib; {\n                parentDataset = mkOption {\n                  type = types.str;\n                  default = \"rpool/local/vms\";\n                  description =\n                    \"the parent dataset to grant the waifud group zfs management access on\";\n                };\n\n                sshKeys = mkOption {\n                  type = with types; listOf str;\n                  default = [ ];\n                  description =\n                    \"the list of SSH public keys to allow waifud to ssh in as\";\n                };\n              };\n\n              config = {\n                environment.defaultPackages = with pkgs; [ qemu zfs wget ];\n                virtualisation.libvirtd.enable = lib.mkDefault true;\n\n                systemd.services.waifud-runner-setup = {\n                  wantedBy = [ \"multi-user.target\" ];\n                  serviceConfig.Type = \"oneshot\";\n                  script = ''\n                    /run/current-system/sw/bin/zfs allow -g waifud create,destroy,mount,snapshot,rollback ${cfg.parentDataset}\n                  '';\n                };\n\n                security.polkit.extraConfig = ''\n                  /* Allow users in the waifud group to manage the libvirt daemon without authentication */\n                  polkit.addRule(function(action, subject) {\n                      if (action.id == \"org.libvirt.unix.manage\" && subject.isInGroup(\"waifud\")) {\n                              return polkit.Result.YES;\n                      }\n                  });\n                '';\n\n                users.users.waifud.openssh.authorizedKeys.keys = cfg.sshKeys;\n\n                security.sudo.extraRules = [{\n                  groups = [ \"waifud\" ];\n                  users = [ \"waifud\" ];\n                  runAs = \"root:root\";\n                  commands = [{\n                    command = \"/run/current-system/sw/bin/qemu-img\";\n                    options = [ \"NOPASSWD\" ];\n                  }];\n                }];\n              };\n            };\n        };\n\n        devShell = with pkgs;\n          mkShell {\n            buildInputs = [\n              cargo\n              cargo-watch\n              rustc\n              rustfmt\n              rust-analyzer\n              pre-commit\n              rustPackages.clippy\n              openssl\n              pkg-config\n              sqlite-interactive\n              libvirt\n              dhall\n              dhall-json\n              jq\n              jo\n              deno\n              strace\n            ];\n            DATABASE_URL = \"./var/waifud.db\";\n            RUST_LOG = \"tower_http=trace,debug\";\n            RUST_SRC_PATH = rustPlatform.rustLibSrc;\n          };\n      });\n}\n"
  },
  {
    "path": "frontend/build.sh",
    "content": "#!/usr/bin/env nix-shell\n#! nix-shell -p deno -i bash\n\nset -e\ncd $(dirname $0)\n\nDENO_FLAGS='--import-map=./import_map.json --lock deno.lock'\n\nif [ \"$1\" == \"--dev\" ]; then\n\tDENO_FLAGS=\"$DENO_FLAGS --watch\"\nfi\n\nexport RUST_LOG=info\n\ndeno cache --import-map=./import_map.json --lock deno.lock --lock-write *.tsx deps.ts\n\nmkdir -p ./static/js\ndeno bundle $DENO_FLAGS ./instance_detail.tsx ./static/js/instance_detail.js &\ndeno bundle $DENO_FLAGS ./instance_create.tsx ./static/js/instance_create.js &\n\nwait\n"
  },
  {
    "path": "frontend/css/build.sh",
    "content": "#!/usr/bin/env nix-shell\n#! nix-shell -p nodePackages.clean-css-cli -i bash\n\ncleancss -o ./xess.css ./src/xess.css ./src/admin.css\n"
  },
  {
    "path": "frontend/css/src/admin.css",
    "content": ".breadcrumb {\n    padding: 0 .5rem;\n}\n\n.breadcrumb ul {\n    display: flex;\n    flex-wrap: wrap;\n    list-style: none;\n    margin: 0;\n    padding: 0;\n}\n\n.breadcrumb li:not(:last-child)::after {\n    display: inline-block;\n    margin: 0 .25rem;\n    content: \"/\";\n}\n\n.left {\n    float: left;\n}\n\n.right {\n    float: right;\n}\n"
  },
  {
    "path": "frontend/css/src/xess.css",
    "content": "@import url(\"https://cdn.xeiaso.net/static/css/iosevka/family.css\");\n\nmain {\n  font-family: Iosevka Aile Iaso, sans-serif;\n  max-width: 50rem;\n  padding: 2rem;\n  margin: auto;\n}\n\n@media only screen and (max-device-width: 736px) {\n  main {\n    padding: 0rem;\n  }\n}\n\n::selection {\n  background: #d3869b;\n}\n\nbody {\n  background: #282828;\n  color:      #ebdbb2;\n}\n\npre {\n  background-color: #3c3836;\n  padding: 1em;\n  border: 0;\n  font-family: Iosevka Curly Iaso, monospace;\n}\n\na, a:active, a:visited {\n  color: #b16286;\n  background-color: #1d2021;\n}\n\nh1, h2, h3, h4, h5 {\n    margin-bottom: .1rem;\n    font-family: Iosevka Etoile Iaso, serif;\n}\n\nblockquote {\n  border-left: 1px solid #bdae93;\n  margin: 0.5em 10px;\n  padding: 0.5em 10px;\n}\n\nfooter {\n  align: center;\n}\n\n@media (prefers-color-scheme: light) {\n    body {\n        background: #fbf1c7;\n        color:      #3c3836;\n    }\n\n    pre {\n        background-color: #ebdbb2;\n        padding: 1em;\n        border: 0;\n    }\n\n    a, a:active, a:visited {\n        color: #b16286;\n        background-color: #f9f5d7;\n    }\n\n    h1, h2, h3, h4, h5 {\n        margin-bottom: .1rem;\n    }\n\n    blockquote {\n        border-left: 1px solid #655c54;\n        margin: 0.5em 10px;\n        padding: 0.5em 10px;\n    }\n}\n"
  },
  {
    "path": "frontend/css/xess.css",
    "content": "@import url(https://cdn.xeiaso.net/static/css/iosevka/family.css);main{font-family:Iosevka Aile Iaso,sans-serif;max-width:50rem;padding:2rem;margin:auto}@media only screen and (max-device-width:736px){main{padding:0}}::selection{background:#d3869b}body{background:#282828;color:#ebdbb2}pre{background-color:#3c3836;padding:1em;border:0;font-family:Iosevka Curly Iaso,monospace}a,a:active,a:visited{color:#b16286;background-color:#1d2021}h1,h2,h3,h4,h5{margin-bottom:.1rem;font-family:Iosevka Etoile Iaso,serif}blockquote{border-left:1px solid #bdae93;margin:.5em 10px;padding:.5em 10px}footer{align:center}@media (prefers-color-scheme:light){body{background:#fbf1c7;color:#3c3836}pre{background-color:#ebdbb2;padding:1em;border:0}a,a:active,a:visited{color:#b16286;background-color:#f9f5d7}h1,h2,h3,h4,h5{margin-bottom:.1rem}blockquote{border-left:1px solid #655c54;margin:.5em 10px;padding:.5em 10px}}.breadcrumb{padding:0 .5rem}.breadcrumb ul{display:flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}.breadcrumb li:not(:last-child)::after{display:inline-block;margin:0 .25rem;content:\"/\"}.left{float:left}.right{float:right}"
  },
  {
    "path": "frontend/deno.json",
    "content": "{\n    \"compilerOptions\": {\n        \"jsx\": \"react-jsx\",\n        \"jsxImportSource\": \"xeact\",\n    },\n    \"importMap\": \"./import_map.json\",\n}\n"
  },
  {
    "path": "frontend/deps.ts",
    "content": "import * as xeact from \"xeact\";\n\nexport {\n    xeact,\n    //xterm\n};\n"
  },
  {
    "path": "frontend/import_map.json",
    "content": "{\n    \"imports\": {\n        \"xeact\": \"https://xena.greedo.xeserv.us/pkg/xeact/v0.69.71/xeact.ts\",\n        \"xeact/jsx-runtime\": \"https://xena.greedo.xeserv.us/pkg/xeact/v0.69.71/jsx-runtime.js\",\n        \"/\": \"./\",\n        \"./\": \"./\"\n    }\n}\n"
  },
  {
    "path": "frontend/instance_create.tsx",
    "content": "/** @jsxImportSource xeact */\n\nimport { u } from \"xeact\";\nimport {\n  getConfig,\n  getDistros,\n  makeInstance,\n  NewInstance,\n} from \"./waifud/mod.ts\";\n\nconst user_data_template = `#cloud-config\n#vim:syntax=yaml\n\nusers:\n  - name: xe\n    groups: [ wheel ]\n    sudo: [ \"ALL=(ALL) NOPASSWD:ALL\" ]\n    shell: /bin/bash\n`;\n\nexport const Page = async () => {\n  const distros = await getDistros();\n  const config = await getConfig();\n\n  const nameBox = <input type=\"text\" placeholder=\"crobat\" />;\n  const memoryBox = <input type=\"text\" placeholder=\"512\" />;\n  const cpuBox = <input type=\"text\" placeholder=\"2\" />;\n  const host = (\n    <select>\n      {config.hosts.map(host => <option value={host}>{host}</option>)}\n    </select>\n  );\n  const disk_size_gb = <input type=\"text\" value=\"25\" />;\n  const zvol_prefix = <input type=\"text\" value=\"rpool/local/vms\" />;\n  const distro = (\n    <select id=\"selectBox\">\n      {distros.map((d) => <option value={d.name}>{d.name}</option>)}\n    </select>\n  );\n  distro.onchange = () => {\n    let selectedDistro: any | null = null;\n    distros.forEach((d) => {\n      if (d.name == distro.value) {\n        selectedDistro = d;\n      }\n    });\n\n    if (selectedDistro == null) {\n      console.log(\n        \"this shouldn't happen, selected distro doesn't exist in our list??\",\n      );\n      return;\n    }\n\n    const disk_size = parseInt(disk_size_gb.value, 10);\n    if (disk_size < selectedDistro.minSize) {\n      disk_size_gb.value = `${selectedDistro.minSize}`;\n    }\n  };\n  const user_data = (\n    <textarea rows=\"10\" cols=\"40\">{user_data_template}</textarea>\n  );\n  const join_tailnet = <input type=\"checkbox\" checked=\"true\" />;\n\n  const submit = <button>Create that sucker</button>;\n  submit.onclick = async () => {\n    const req: NewInstance = {\n      name: nameBox.value != \"\" ? nameBox.value : undefined,\n      memory_mb: memoryBox.value != \"\"\n        ? parseInt(memoryBox.value, 10)\n        : undefined,\n      cpus: cpuBox.value != \"\" ? parseInt(cpuBox.value, 10) : undefined,\n      host: host.value,\n      disk_size_gb: disk_size_gb.value != \"\"\n        ? parseInt(disk_size_gb.value, 10)\n        : undefined,\n      zvol_prefix: zvol_prefix.value != \"\" ? zvol_prefix.value : undefined,\n      distro: distro.value,\n      user_data: user_data.value,\n      join_tailnet: join_tailnet.checked,\n    };\n    console.log(req);\n    const instance = await makeInstance(req);\n    console.log(instance);\n    window.location.href = u(`/admin/instances/${instance.uuid}`);\n  };\n\n  return (\n    <div>\n      <table>\n        <tr>\n          <th>Name</th>\n          <td>{nameBox}</td>\n        </tr>\n        <tr>\n          <th>Memory (MB)</th>\n          <td>{memoryBox}</td>\n        </tr>\n        <tr>\n          <th>CPU cores</th>\n          <td>{cpuBox}</td>\n        </tr>\n        <tr>\n          <th>Host</th>\n          <td>{host}</td>\n        </tr>\n        <tr>\n          <th>Disk size (GB)</th>\n          <td>{disk_size_gb}</td>\n        </tr>\n        <tr>\n          <th>ZVol prefix</th>\n          <td>{zvol_prefix}</td>\n        </tr>\n        <tr>\n          <th>Distro</th>\n          <td>{distro}</td>\n        </tr>\n        <tr>\n          <th>Userdata</th>\n          <td>{user_data}</td>\n        </tr>\n        <tr>\n          <th>Join tailnet + SSH?</th>\n          <td>{join_tailnet}</td>\n        </tr>\n        <tr>\n          <td>{\"\"}</td>\n        </tr>\n        <tr>\n          <td>{submit}</td>\n        </tr>\n      </table>\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/instance_detail.tsx",
    "content": "/** @jsxImportSource xeact */\n\nexport function Fragment({ children }: { children: any[] }): any[] {\n  return children;\n}\n\nimport { g, t, u } from \"xeact\";\nimport {\n  deleteInstance,\n  getAuditLogsForInstance,\n  hardRebootInstance,\n  rebootInstance,\n  reinitInstance,\n  shutdownInstance,\n  startInstance,\n} from \"./waifud/mod.ts\";\n\ntype InstanceButtonProps = {\n  text: string;\n  instance_id: string;\n  action: string;\n  message: string;\n  confirm?: boolean;\n};\n\nfunction DeleteInstanceButton(\n  { text, instance_id, message, confirm = true }: InstanceButtonProps,\n) {\n  const onclick = async () => {\n    if (confirm) {\n      const response = prompt(\n        \"Type 'I don't care about the data' to continue.\",\n      );\n      if (response !== \"I don't care about the data\") {\n        g(\"messages\").appendChild(t(\"Confirmation failed.\"));\n        return;\n      }\n    }\n    await deleteInstance(instance_id);\n    g(\"messages\").appendChild(t(message));\n    alert(message);\n    window.location.href = u(\"/admin/instances\");\n  };\n  return (\n    <div>\n      <button onclick={() => onclick()}>{text}</button>\n      <br />\n    </div>\n  );\n}\n\nfunction InstanceButton(\n  { text, instance_id, action, message, confirm = false }: InstanceButtonProps,\n) {\n  const onclick = async () => {\n    if (confirm) {\n      const response = prompt(\n        \"Type 'I don't care about the data' to continue.\",\n      );\n      if (response !== \"I don't care about the data\") {\n        g(\"messages\").appendChild(t(\"Confirmation failed.\"));\n        return;\n      }\n    }\n    switch (action) {\n      case \"reboot\":\n        await rebootInstance(instance_id);\n        break;\n      case \"hardreboot\":\n        await hardRebootInstance(instance_id);\n        break;\n      case \"reinit\":\n        await reinitInstance(instance_id);\n        break;\n      case \"shutdown\":\n        await shutdownInstance(instance_id);\n        break;\n      case \"start\":\n        await startInstance(instance_id);\n        break;\n    }\n    g(\"messages\").appendChild(t(message));\n  };\n  return (\n    <div>\n      <button onclick={() => onclick()}>{text}</button>\n      <br />\n    </div>\n  );\n}\n\nexport async function Page() {\n  const instance_id = g(\"instance_id\").innerText;\n  const auditLogs = (await getAuditLogsForInstance(instance_id)).map((al) => (\n    <tr>\n      <td>{new Date(al.ts * 1000).toLocaleString()}</td>\n      <td>{al.op}</td>\n    </tr>\n  ));\n  auditLogs.unshift(\n    <tr>\n      <th>Time</th>\n      <th>Operation</th>\n    </tr>,\n  );\n\n  return (\n    <div>\n      <InstanceButton\n        text=\"Reboot\"\n        instance_id={instance_id}\n        action=\"reboot\"\n        message=\"VM Rebooted.\"\n      />\n      <InstanceButton\n        text=\"Hard Reboot\"\n        instance_id={instance_id}\n        action=\"hardreboot\"\n        message=\"VM hard-rebooted.\"\n      />\n      <InstanceButton\n        text=\"Recreate VM\"\n        instance_id={instance_id}\n        action=\"reinit\"\n        message=\"Recreating VM from scratch.\"\n        confirm={true}\n      />\n      <InstanceButton\n        text=\"Shutdown\"\n        instance_id={instance_id}\n        action=\"shutdown\"\n        message=\"VM shut down.\"\n      />\n      <InstanceButton\n        text=\"Start\"\n        instance_id={instance_id}\n        action=\"start\"\n        message=\"VM Started.\"\n      />\n      <DeleteInstanceButton\n        text=\"Delete instance\"\n        instance_id={instance_id}\n        action=\"delete\"\n        message=\"Instance deleted, redirecting you to instances page.\"\n      />\n      <div>\n        <h3>Audit Logs</h3>\n        <table>\n          {auditLogs}\n        </table>\n      </div>\n      <div id=\"messages\">\n        <h3>Messages</h3>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/static/js/.gitignore",
    "content": "*.js\n"
  },
  {
    "path": "frontend/waifud/mod.ts",
    "content": "import { u } from \"xeact\";\n\nexport type Config = {   \n    base_url: string,\n    hosts: string[],\n    bind_host: string,\n    port: number,\n    rpool_base: string,\n    qemu_path: string,\n};\n\nexport const getConfig = async (): Promise<Config> => {\n    const resp = await fetch(u(\"/admin/api/config\"));\n    if (resp.status !== 200) {\n        const body = await resp.text();\n        throw new Error(\"wrong status code: \" + resp.status + \"\\n\\n\" + body);\n    }\n\n    const result: Config = await resp.json();\n    return result;\n}\n\nexport type Distro = {\n    name: string;\n    downloadURL: string;\n    sha256Sum: string;\n    minSize: string;\n    format: string;\n};\n\nexport const getDistros = async (): Promise<Distro[]> => {\n    const resp = await fetch(u(\"/api/v1/distros\"));\n    if (resp.status !== 200) {\n        const body = await resp.text();\n        throw new Error(\"wrong status code: \" + resp.status + \"\\n\\n\" + body);\n    }\n\n    const result: Distro[] = await resp.json();\n\n    return result;\n};\n\nexport type AuditLog = {\n    id: number;\n    ts: number;\n    kind: string;\n    op: string;\n    data: any;\n    uuid?: string;\n    name?: string;\n};\n\nexport const getAuditLogs = async (): Promise<AuditLog[]> => {\n    const resp = await fetch(u(\"/api/v1/auditlogs\"));\n    if (resp.status !== 200) {\n        const body = await resp.text();\n        throw new Error(\"wrong status code: \" + resp.status + \"\\n\\n\" + body);\n    }\n\n    const result: AuditLog[] = await resp.json();\n\n    return result;\n};\n\nexport const getAuditLogsForInstance = async (id: string): Promise<AuditLog[]> => {\n    const resp = await fetch(u(`/api/v1/auditlogs/instance/${id}`));\n    if (resp.status !== 200) {\n        const body = await resp.text();\n        throw new Error(\"wrong status code: \" + resp.status + \"\\n\\n\" + body);\n    }\n\n    const result: AuditLog[] = await resp.json();\n\n    return result;\n};\n\nexport type NewInstance = {\n    name?: string;\n    memory_mb?: number;\n    cpus?: number;\n    host: string;\n    disk_size_gb?: number;\n    zvol_prefix?: string;\n    distro: string;\n    user_data?: string;\n    join_tailnet: boolean;\n};\n\nexport type Instance = {\n    uuid: string;\n    name: string;\n    host: string;\n    mac_address: string;\n    memory: number;\n    disk_size: number;\n    zvol_name: string;\n    status: string;\n    distro: string;\n    join_tailnet: boolean;\n};\n\nexport const makeInstance = async (ni: NewInstance): Promise<Instance> => {\n    const resp = await fetch(u(\"/api/v1/instances\"), {\n        method: \"POST\",\n        body: JSON.stringify(ni),\n        headers: {\n            \"Accept\": \"application/json\",\n            \"Content-Type\": \"application/json\",\n        },\n    });\n\n    if (resp.status !== 200) {\n        const body = await resp.text();\n        throw new Error(\"wrong status code: \" + resp.status + \"\\n\\n\" + body);\n    }\n\n    const instance: Instance = await resp.json();\n    return instance;\n};\n\nexport const deleteInstance = async (id: string): Promise<void> => {\n    await fetch(u(`/api/v1/instances/${id}`), {\n        method: \"DELETE\",\n    });\n}\n\nconst doThingToInstance = (action: string): (id: string) => Promise<void> => {\n    return (async (id: string): Promise<void> => {       \n        const resp = await fetch(u(`/api/v1/instances/${id}/${action}`), {\n            method: \"POST\",\n        });\n\n        if (resp.status !== 200) {\n            const body = await resp.text();\n            throw new Error(\"wrong status code: \" + resp.status + \"\\n\\n\" + body);\n        }\n    });\n}\n\nexport const rebootInstance = doThingToInstance(\"reboot\");\nexport const hardRebootInstance = doThingToInstance(\"hardreboot\");\nexport const reinitInstance = doThingToInstance(\"reinit\");\nexport const shutdownInstance = doThingToInstance(\"shutdown\");\nexport const startInstance = doThingToInstance(\"start\");\n"
  },
  {
    "path": "lib/rotbart/Cargo.toml",
    "content": "[package]\nname = \"rotbart\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [ \"Xe Iaso <me@xeiaso.net>\" ]\nrepository = \"https://github.com/Xe/waifud\"\nlicense = \"mit\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nlazy_static = \"1\"\nnames = \"0.14\"\n"
  },
  {
    "path": "lib/rotbart/scrapers/README.md",
    "content": "# How to generate names.json\n\nOpen https://xenoblade.github.io/xb2/bdat/common/BLD_NameList.html and paste\nthis into the browser inspector:\n\n```js\nnames = [];\nArray.from(document.getElementsByClassName(\"sortable\")[0].children[1].children)\n  .forEach(row => names.push(row.children[2]\n    .innerHTML\n    .toLowerCase()\n    .replaceAll(\" \", \"-\")));\nconsole.log(JSON.stringify(names));\n```\n\nThen format it with jq.\n\nFor ponies use this fragment:\n\n```javascript\nnames = [];\nArray.from(document.getElementsByClassName(\"listofponies\")[0]\n  .children[1]\n  .children\n).forEach(row => {\n  let name = row.children[0]\n    .textContent\n    .toLowerCase()\n    .replaceAll(\" \", \"-\")\n    .replaceAll(\".\", \"\")\n    .replaceAll(\"ö\", \"o\");\n  if (name.includes(\"unnamed\")) { return; }\n  if (name.includes(\"[\")) { return; }\n  if (name.includes(\"/\")) { return; }\n  if (name.includes(\"alt\")) { return; }\n  if (name.includes(\"pony\")) { return; }\n  if (name.includes(\"mare\")) { return; }\n  if (name.includes(\"student\")) { return; }\n  if (name.includes(\"'\")) { return; }\n  if (name.includes('\"')) { return; }\n  if (name.length > 10) { return; }\n  console.log([name, name.length]);\n  names.push(name);\n});\nconsole.log(JSON.stringify(names));\n```\n\nCombine all of the files like this:\n\n```console\n$ jq -n '[inputs] | add' \\\n    blaseball.json \\\n    names-blades.json \\\n    names-ponies-earth.json \\\n    names-ponies-pegasus.json \\\n    names-ponies-unicorn.json \\\n    pokemon.json \\\n    pokemon-hisui.json \\\n  | jq -r '.[]' \\\n  | sort \\\n  | uniq \\\n  | jq -nR '[inputs | select(length>0)]' > names.json\n```\n"
  },
  {
    "path": "lib/rotbart/scrapers/blaseball.sh",
    "content": "#!/usr/bin/env nix-shell\n#! nix-shell -p jq -p curl -i bash\n\ncurl 'https://api.sibr.dev/chronicler/v2/entities?type=player&at=2020-11-01T00:00:00Z' \\\n   | jq '.items[].data.name' -r \\\n   | grep -v -- \"-\" \\\n   | tr '[:upper:]' '[:lower:]' \\\n   | tr ' ' '-' \\\n   | sed 's/\\.//g' \\\n   | sed 's/://g' \\\n   | jq --raw-input '.' \\\n   | jq -s > blaseball.json\n"
  },
  {
    "path": "lib/rotbart/scrapers/pokedex-hisui.sh",
    "content": "#!/usr/bin/env nix-shell\n#! nix-shell -p jq -p curl -i bash\n\ncat pokedex-hisui.json \\\n    | jq -r '.[].name' \\\n    | tr '[:upper:]' '[:lower:]' \\\n    | tr ' ' '-' \\\n    | sed 's/\\.//g' \\\n    | sed 's/://g' \\\n    | jq --raw-input '.' \\\n    | jq -s > pokemon-hisui.json\n"
  },
  {
    "path": "lib/rotbart/scrapers/pokedex.sh",
    "content": "#!/usr/bin/env nix-shell\n#! nix-shell -p jq -p curl -i bash\n\ncurl https://raw.githubusercontent.com/fanzeyi/pokemon.json/master/pokedex.json \\\n    | jq -r '.[].name.english' \\\n    | tr '[:upper:]' '[:lower:]' \\\n    | tr ' ' '-' \\\n    | sed 's/\\.//g' \\\n    | sed 's/://g' \\\n    | jq --raw-input '.' \\\n    | jq -s > pokemon.json\n"
  },
  {
    "path": "lib/rotbart/src/blaseball.rs",
    "content": "pub const FIRST_NAMES: &'static [&'static str] = &[\n    \"abbie\",\n    \"abbott\",\n    \"abner\",\n    \"acosta\",\n    \"adalberto\",\n    \"adelaide\",\n    \"adeline\",\n    \"adi\",\n    \"adkins\",\n    \"adrian\",\n    \"adrianna\",\n    \"agan\",\n    \"agnes\",\n    \"agustín\",\n    \"aisha\",\n    \"aitor\",\n    \"alaynabella\",\n    \"albert\",\n    \"aldo\",\n    \"aldon\",\n    \"alejandro\",\n    \"alex\",\n    \"alexander\",\n    \"alexandria\",\n    \"alexi\",\n    \"alford\",\n    \"ali\",\n    \"alison\",\n    \"allan\",\n    \"allis\",\n    \"allison\",\n    \"almond\",\n    \"alston\",\n    \"alvie\",\n    \"alvis\",\n    \"alx\",\n    \"alyssa\",\n    \"amal\",\n    \"amaya\",\n    \"amias\",\n    \"amos\",\n    \"anabela\",\n    \"anaroniku\",\n    \"anastasia\",\n    \"anathema\",\n    \"anaximandra\",\n    \"andrew\",\n    \"anemone\",\n    \"aneurin\",\n    \"ankle\",\n    \"anna\",\n    \"annick\",\n    \"annie\",\n    \"anthony\",\n    \"antonio\",\n    \"aoife\",\n    \"apollo\",\n    \"arantxa\",\n    \"arches\",\n    \"archie\",\n    \"ardy\",\n    \"ariadne\",\n    \"armen\",\n    \"artemesia\",\n    \"arthur\",\n    \"arturo\",\n    \"arvin\",\n    \"astrothesia\",\n    \"athena\",\n    \"atlas\",\n    \"atma\",\n    \"attila\",\n    \"aubrey\",\n    \"august\",\n    \"augusta\",\n    \"augusto\",\n    \"aureliano\",\n    \"aurora\",\n    \"avi\",\n    \"avila\",\n    \"axel\",\n    \"ayanna\",\n    \"aymer\",\n    \"bq\",\n    \"baby\",\n    \"backpatch\",\n    \"badger\",\n    \"badgerson\",\n    \"baldwin\",\n    \"balina\",\n    \"balthazar\",\n    \"bambi\",\n    \"bandit\",\n    \"bao\",\n    \"barney\",\n    \"barry\",\n    \"bartleby\",\n    \"bash\",\n    \"basil\",\n    \"basilio\",\n    \"bates\",\n    \"bauer\",\n    \"beans\",\n    \"beasley\",\n    \"beau\",\n    \"beck\",\n    \"becker\",\n    \"bees\",\n    \"belinda\",\n    \"ben\",\n    \"bengi\",\n    \"benjamin\",\n    \"bennett\",\n    \"benny\",\n    \"benson\",\n    \"bernie\",\n    \"bert\",\n    \"best\",\n    \"bethel\",\n    \"betsy\",\n    \"bevan\",\n    \"bevis\",\n    \"billup\",\n    \"bistro\",\n    \"blaire\",\n    \"blake\",\n    \"blankenship\",\n    \"blimp\",\n    \"blondie\",\n    \"blood\",\n    \"bloom\",\n    \"blossom\",\n    \"bob\",\n    \"bobbin\",\n    \"boden\",\n    \"bogan\",\n    \"bones\",\n    \"bonito\",\n    \"bonk\",\n    \"bonnie\",\n    \"borg\",\n    \"bortimus\",\n    \"bottles\",\n    \"boudicca\",\n    \"boyd\",\n    \"boyfriend\",\n    \"brad\",\n    \"branson\",\n    \"breckon\",\n    \"bree\",\n    \"brewer\",\n    \"bright\",\n    \"brimtley\",\n    \"brisket\",\n    \"brock\",\n    \"bront\",\n    \"brooke\",\n    \"bruno\",\n    \"bryanayah\",\n    \"bryce\",\n    \"brynn\",\n    \"buck\",\n    \"buddy\",\n    \"burke\",\n    \"buster\",\n    \"butch\",\n    \"byron\",\n    \"byung-hyun\",\n    \"cactus\",\n    \"cadence\",\n    \"caim\",\n    \"caleb\",\n    \"caligula\",\n    \"campos\",\n    \"cannonball\",\n    \"cantus\",\n    \"cardamom\",\n    \"carmelo\",\n    \"carol\",\n    \"carrol\",\n    \"carter\",\n    \"case\",\n    \"cass\",\n    \"cassidy\",\n    \"castillo\",\n    \"cat\",\n    \"catmint\",\n    \"cedric\",\n    \"celeste\",\n    \"celestial\",\n    \"cell\",\n    \"celo\",\n    \"chadwick\",\n    \"chambers\",\n    \"chandra\",\n    \"charlatan\",\n    \"chester\",\n    \"chet\",\n    \"chibodee\",\n    \"chip\",\n    \"chips\",\n    \"chorby\",\n    \"chris\",\n    \"christian\",\n    \"churro\",\n    \"cicero\",\n    \"cindy\",\n    \"cinnamon\",\n    \"cissy\",\n    \"clare\",\n    \"claudio\",\n    \"clementine\",\n    \"clodius\",\n    \"clove\",\n    \"cody\",\n    \"collins\",\n    \"colton\",\n    \"combs\",\n    \"comfort\",\n    \"commissioner\",\n    \"concrete\",\n    \"conditional\",\n    \"conner\",\n    \"conrad\",\n    \"coolname\",\n    \"corbyn\",\n    \"cordula\",\n    \"cornelius\",\n    \"corriander\",\n    \"cory\",\n    \"cote\",\n    \"cravel\",\n    \"cricket\",\n    \"crits\",\n    \"crow\",\n    \"cudi\",\n    \"curry\",\n    \"dabney\",\n    \"daiya\",\n    \"damir\",\n    \"dander\",\n    \"dani\",\n    \"daniel\",\n    \"danny\",\n    \"darren\",\n    \"dash\",\n    \"dashiell\",\n    \"dave\",\n    \"davena\",\n    \"david\",\n    \"davie\",\n    \"dax\",\n    \"deangelo\",\n    \"deandre\",\n    \"declan\",\n    \"demarkus\",\n    \"denim\",\n    \"denzel\",\n    \"derrick\",\n    \"dervin\",\n    \"devon\",\n    \"dexter\",\n    \"dickerson\",\n    \"didi\",\n    \"dimi\",\n    \"discovery\",\n    \"djuna\",\n    \"doc\",\n    \"doginic\",\n    \"dolores\",\n    \"dominic\",\n    \"domino\",\n    \"don\",\n    \"donia\",\n    \"donna\",\n    \"donnie\",\n    \"douglas\",\n    \"dovydas\",\n    \"drea\",\n    \"drew\",\n    \"drosophila\",\n    \"dudley\",\n    \"dulce\",\n    \"duncan\",\n    \"dunlap\",\n    \"dunn\",\n    \"durham\",\n    \"ed\",\n    \"eddie\",\n    \"eden\",\n    \"edith\",\n    \"edric\",\n    \"eduardo\",\n    \"eizabeth\",\n    \"ekeko\",\n    \"elijah\",\n    \"eliot\",\n    \"elip\",\n    \"elisisor\",\n    \"ellen\",\n    \"ellie\",\n    \"elliot\",\n    \"elroy\",\n    \"elsha\",\n    \"elvis\",\n    \"elwin\",\n    \"emblem\",\n    \"emilia\",\n    \"emmet\",\n    \"emmett\",\n    \"emmy\",\n    \"engine\",\n    \"england\",\n    \"enid\",\n    \"ennead\",\n    \"ephraim\",\n    \"erica\",\n    \"erickson\",\n    \"erin\",\n    \"eris\",\n    \"esme\",\n    \"esteban\",\n    \"euclid\",\n    \"eudora\",\n    \"eugenia\",\n    \"eurico\",\n    \"evelton\",\n    \"everett\",\n    \"ezekiel\",\n    \"fairwood\",\n    \"famous\",\n    \"faraday\",\n    \"farrell\",\n    \"feline\",\n    \"felix\",\n    \"fenry\",\n    \"finn\",\n    \"fionna\",\n    \"fish\",\n    \"fitzgerald\",\n    \"flannery\",\n    \"flattery\",\n    \"fletcher\",\n    \"florian\",\n    \"fontaine\",\n    \"forbes\",\n    \"forrest\",\n    \"foxy\",\n    \"fran\",\n    \"francisca\",\n    \"francisco\",\n    \"francois\",\n    \"frank\",\n    \"frankie\",\n    \"françois\",\n    \"frasier\",\n    \"frazier\",\n    \"freemium\",\n    \"fynn\",\n    \"gabriel\",\n    \"gallup\",\n    \"garcia\",\n    \"geepa\",\n    \"geordi\",\n    \"georgina\",\n    \"geraldine\",\n    \"gerund\",\n    \"gia\",\n    \"gib\",\n    \"ginny\",\n    \"gita\",\n    \"gizmo\",\n    \"glabe\",\n    \"gloria\",\n    \"goeff\",\n    \"goldy\",\n    \"golem\",\n    \"gomer\",\n    \"goobie\",\n    \"goodwin\",\n    \"grant\",\n    \"greer\",\n    \"gregroy\",\n    \"grey\",\n    \"grimbo\",\n    \"grit\",\n    \"grollis\",\n    \"guadalupe\",\n    \"gunther\",\n    \"gustavo\",\n    \"guy\",\n    \"gwen\",\n    \"göran\",\n    \"hadi\",\n    \"hadleigh\",\n    \"hahn\",\n    \"halexandrey\",\n    \"haman\",\n    \"hands\",\n    \"hank\",\n    \"hans\",\n    \"hapless\",\n    \"harmon\",\n    \"harold\",\n    \"harper\",\n    \"harriet\",\n    \"harrington\",\n    \"harry\",\n    \"haruta\",\n    \"hatfield\",\n    \"hazel\",\n    \"helga\",\n    \"hen\",\n    \"hendricks\",\n    \"henevieve\",\n    \"henry\",\n    \"hercules\",\n    \"hernando\",\n    \"herring\",\n    \"hewitt\",\n    \"hierophantic\",\n    \"higgins\",\n    \"hildegard\",\n    \"hillary\",\n    \"hiroto\",\n    \"hoagie\",\n    \"hobbs\",\n    \"holden\",\n    \"hongo\",\n    \"hops\",\n    \"hotbox\",\n    \"howell\",\n    \"howie\",\n    \"huber\",\n    \"hubert\",\n    \"hugs\",\n    \"hui\",\n    \"hurley\",\n    \"hyena\",\n    \"hyo-jin\",\n    \"icarus\",\n    \"ignacio\",\n    \"igneus\",\n    \"ilane\",\n    \"ilhan\",\n    \"inez\",\n    \"ingrid\",\n    \"inky\",\n    \"ira\",\n    \"irnee\",\n    \"isaac\",\n    \"isabella\",\n    \"itsuki\",\n    \"izuki\",\n    \"jack\",\n    \"jackie\",\n    \"jackson\",\n    \"jacob\",\n    \"jacobus\",\n    \"jacoby\",\n    \"jada\",\n    \"jade\",\n    \"jake\",\n    \"jam\",\n    \"jame\",\n    \"james\",\n    \"jammy\",\n    \"jan\",\n    \"jana\",\n    \"janet\",\n    \"jaron\",\n    \"jasmine\",\n    \"jasper\",\n    \"javier\",\n    \"jaxon\",\n    \"jay\",\n    \"jayden\",\n    \"jaylen\",\n    \"jebediah\",\n    \"jeff\",\n    \"jefferson\",\n    \"jeffier\",\n    \"jeffrey\",\n    \"jelly\",\n    \"jem\",\n    \"jenkins\",\n    \"jenna\",\n    \"jenny\",\n    \"jesse\",\n    \"jessi\",\n    \"jessica\",\n    \"jesus\",\n    \"jesús\",\n    \"ji-tae\",\n    \"jim\",\n    \"jimbo\",\n    \"jo\",\n    \"joana\",\n    \"jode\",\n    \"joe\",\n    \"joel\",\n    \"joey\",\n    \"johannes\",\n    \"johncy\",\n    \"johndan\",\n    \"johnny\",\n    \"johnnyboy\",\n    \"jolene\",\n    \"jomgy\",\n    \"jon\",\n    \"jonathan\",\n    \"jordan\",\n    \"jorge\",\n    \"jose\",\n    \"joshua\",\n    \"jose\",\n    \"jot\",\n    \"joyner\",\n    \"juan\",\n    \"juice\",\n    \"juju\",\n    \"julianne\",\n    \"june\",\n    \"junior\",\n    \"justice\",\n    \"justin\",\n    \"jut\",\n    \"jeff\",\n    \"kaelin\",\n    \"kai\",\n    \"kaiba\",\n    \"kaiden\",\n    \"kaj\",\n    \"kale\",\n    \"kaloni\",\n    \"kang-min\",\n    \"karato\",\n    \"karlee\",\n    \"kathy\",\n    \"katy\",\n    \"kay\",\n    \"kaylah\",\n    \"kaz\",\n    \"keanu\",\n    \"keeley\",\n    \"kelbasa\",\n    \"kels\",\n    \"kelvin\",\n    \"kennedy\",\n    \"kenny\",\n    \"kevelyn\",\n    \"kevin\",\n    \"khaanyo\",\n    \"khalid\",\n    \"khulan\",\n    \"kichiro\",\n    \"kiki\",\n    \"kina\",\n    \"king\",\n    \"kirkland\",\n    \"kit\",\n    \"kline\",\n    \"knight\",\n    \"kofi\",\n    \"krzysztof\",\n    \"kurt\",\n    \"kylie\",\n    \"lachlan\",\n    \"lady\",\n    \"lance\",\n    \"lancelot\",\n    \"landry\",\n    \"lang\",\n    \"langley\",\n    \"lanie\",\n    \"lars\",\n    \"lawrence\",\n    \"layla\",\n    \"leach\",\n    \"lee\",\n    \"legory\",\n    \"leif\",\n    \"leliel\",\n    \"len\",\n    \"lenix\",\n    \"lenjamin\",\n    \"lenny\",\n    \"leo\",\n    \"les\",\n    \"leticia\",\n    \"lev\",\n    \"lexi\",\n    \"liam\",\n    \"lili\",\n    \"lily\",\n    \"linda\",\n    \"linnea\",\n    \"linus\",\n    \"lis\",\n    \"livers\",\n    \"livi\",\n    \"lizzy\",\n    \"logan\",\n    \"london\",\n    \"lorcan\",\n    \"lorenzo\",\n    \"lori\",\n    \"lottie\",\n    \"lotus\",\n    \"lou\",\n    \"loubert\",\n    \"lowe\",\n    \"lucas\",\n    \"lucien\",\n    \"lucy\",\n    \"lucy-rose\",\n    \"luis\",\n    \"luka\",\n    \"luna\",\n    \"lurlene\",\n    \"lydia\",\n    \"lyndsey\",\n    \"lyra\",\n    \"madeline\",\n    \"magi\",\n    \"magie\",\n    \"mags\",\n    \"maisy\",\n    \"malachi\",\n    \"malcolm\",\n    \"malik\",\n    \"malin\",\n    \"mambo\",\n    \"manjula\",\n    \"manu\",\n    \"map\",\n    \"marcellus\",\n    \"marco\",\n    \"marf\",\n    \"margarito\",\n    \"mariana\",\n    \"marion\",\n    \"markel\",\n    \"marley\",\n    \"marquez\",\n    \"math\",\n    \"matheo\",\n    \"matteo\",\n    \"mattias\",\n    \"mavis\",\n    \"mayra\",\n    \"mcbaseball\",\n    \"mckinley\",\n    \"mccormick\",\n    \"mcdowell\",\n    \"mcfarland\",\n    \"mckinney\",\n    \"mclaughlin\",\n    \"meera\",\n    \"megan\",\n    \"mel\",\n    \"melba\",\n    \"melton\",\n    \"memorial\",\n    \"mesmer\",\n    \"meteora\",\n    \"mia\",\n    \"miah\",\n    \"michael\",\n    \"michelle\",\n    \"mickey\",\n    \"mieke\",\n    \"miguel\",\n    \"mikan\",\n    \"mike\",\n    \"miki\",\n    \"milan\",\n    \"miles\",\n    \"milli\",\n    \"millipede\",\n    \"milner\",\n    \"milo\",\n    \"min-hyuk\",\n    \"minato\",\n    \"mindy\",\n    \"minnie\",\n    \"mint\",\n    \"mira\",\n    \"mo\",\n    \"mohammed\",\n    \"moira\",\n    \"mokena\",\n    \"mononymous\",\n    \"montgomery\",\n    \"moody\",\n    \"mooney\",\n    \"mordecai\",\n    \"morgan\",\n    \"morrow\",\n    \"morte\",\n    \"moses\",\n    \"muggsy\",\n    \"mullen\",\n    \"mummy\",\n    \"munavoi\",\n    \"munro\",\n    \"murphy\",\n    \"murray\",\n    \"muse\",\n    \"nagomi\",\n    \"nanci\",\n    \"nandy\",\n    \"natalie\",\n    \"natha\",\n    \"neerie\",\n    \"neptunia\",\n    \"nerd\",\n    \"ness\",\n    \"newton\",\n    \"nic\",\n    \"nicholas\",\n    \"nickname\",\n    \"nicky\",\n    \"nicolae\",\n    \"nikki\",\n    \"niq\",\n    \"nitzan\",\n    \"nneka\",\n    \"noah\",\n    \"nolan\",\n    \"nolanestophia\",\n    \"nolastname\",\n    \"noluvuyo\",\n    \"noquiryn\",\n    \"nora\",\n    \"norman\",\n    \"norris\",\n    \"nova\",\n    \"nuan\",\n    \"nucleus\",\n    \"nyx\",\n    \"ogden\",\n    \"ohmar\",\n    \"oliver\",\n    \"ooze\",\n    \"ophelia\",\n    \"orchid\",\n    \"orion\",\n    \"orpheus\",\n    \"ortiz\",\n    \"orville\",\n    \"oscar\",\n    \"ovid\",\n    \"owen\",\n    \"pacheco\",\n    \"paco\",\n    \"paige\",\n    \"palomo\",\n    \"pangolin\",\n    \"pannonica\",\n    \"parker\",\n    \"patchwork\",\n    \"patel\",\n    \"patrick\",\n    \"patty\",\n    \"paul\",\n    \"paula\",\n    \"pavithra\",\n    \"pavo\",\n    \"peanut\",\n    \"peanutiel\",\n    \"pearl\",\n    \"pedro\",\n    \"peekaboo\",\n    \"pelo\",\n    \"pemmy\",\n    \"penelope\",\n    \"penny\",\n    \"pepper\",\n    \"percival\",\n    \"persephone\",\n    \"pete\",\n    \"phil\",\n    \"phineas\",\n    \"pierogi\",\n    \"pierre\",\n    \"pigeon\",\n    \"piper\",\n    \"pippin\",\n    \"planktos\",\n    \"plums\",\n    \"polkadot\",\n    \"pollard\",\n    \"poppy\",\n    \"porkchop\",\n    \"pranav\",\n    \"premjeet\",\n    \"prepper\",\n    \"prince\",\n    \"prophylaxis\",\n    \"pudge\",\n    \"pug\",\n    \"qais\",\n    \"quack\",\n    \"quads\",\n    \"quantum\",\n    \"queithlein\",\n    \"quill\",\n    \"quinns\",\n    \"qwazukee\",\n    \"rafael\",\n    \"rai\",\n    \"ralph\",\n    \"ram\",\n    \"ramirez\",\n    \"randall\",\n    \"randy\",\n    \"rat\",\n    \"ray\",\n    \"razz\",\n    \"razzlynette\",\n    \"raúl\",\n    \"reb\",\n    \"red\",\n    \"reece\",\n    \"reese\",\n    \"reggie\",\n    \"reia\",\n    \"ren\",\n    \"rey\",\n    \"rhombus\",\n    \"rhonda\",\n    \"rhys\",\n    \"rian\",\n    \"richardson\",\n    \"richmond\",\n    \"ridley\",\n    \"rigby\",\n    \"riley\",\n    \"rivers\",\n    \"robbins\",\n    \"robin\",\n    \"rocha\",\n    \"rocio\",\n    \"rodriguez\",\n    \"ron\",\n    \"ronan\",\n    \"ros\",\n    \"rosa\",\n    \"rosales\",\n    \"rosalind\",\n    \"roscoe\",\n    \"rose\",\n    \"rosemary\",\n    \"rosey\",\n    \"ross\",\n    \"rosstin\",\n    \"rudolph\",\n    \"ruffian\",\n    \"rufus\",\n    \"rush\",\n    \"ruslan\",\n    \"russo\",\n    \"rust\",\n    \"ruth\",\n    \"ryan\",\n    \"rylan\",\n    \"ryuji\",\n    \"salamandra\",\n    \"salem\",\n    \"salih\",\n    \"sam\",\n    \"samothes\",\n    \"sandford\",\n    \"sandie\",\n    \"sandoval\",\n    \"santana\",\n    \"saoirse\",\n    \"sapphic\",\n    \"sarahlynn\",\n    \"sassy\",\n    \"scarlet\",\n    \"schneider\",\n    \"schu\",\n    \"scoobert\",\n    \"scoop\",\n    \"scores\",\n    \"scouse\",\n    \"scrap\",\n    \"scratch\",\n    \"scruffs\",\n    \"sebastian\",\n    \"seren\",\n    \"serge\",\n    \"sexton\",\n    \"shane\",\n    \"shannon\",\n    \"shaquille\",\n    \"sheev\",\n    \"shelby\",\n    \"sheri\",\n    \"shirai\",\n    \"shrimp\",\n    \"sigmund\",\n    \"silvaire\",\n    \"silvia\",\n    \"simba\",\n    \"simon\",\n    \"simone\",\n    \"siobhan\",\n    \"sixpack\",\n    \"sleve\",\n    \"slosh\",\n    \"snyder\",\n    \"so-hyun\",\n    \"socks\",\n    \"son\",\n    \"sophie\",\n    \"soraya\",\n    \"sorrell\",\n    \"sosa\",\n    \"sparks\",\n    \"spears\",\n    \"speed\",\n    \"spiff\",\n    \"spits\",\n    \"spradley\",\n    \"squid\",\n    \"squidgey\",\n    \"stan\",\n    \"stanislaw\",\n    \"stasia\",\n    \"stavros\",\n    \"steals\",\n    \"steph\",\n    \"stephanie\",\n    \"stephens\",\n    \"stephon\",\n    \"steve\",\n    \"stevenson\",\n    \"stew\",\n    \"sticky\",\n    \"stijn\",\n    \"stitches\",\n    \"stout\",\n    \"strelitzia\",\n    \"stu\",\n    \"stylianos\",\n    \"suede\",\n    \"sullivan\",\n    \"summers\",\n    \"sunny\",\n    \"suraj\",\n    \"susananana\",\n    \"sutton\",\n    \"swamuel\",\n    \"sweet\",\n    \"sweets\",\n    \"swish\",\n    \"tad\",\n    \"tai\",\n    \"tallulah\",\n    \"tamara\",\n    \"tamsie\",\n    \"tarn\",\n    \"tavin\",\n    \"telusaa\",\n    \"terence\",\n    \"terrell\",\n    \"tevin\",\n    \"theo\",\n    \"theodore\",\n    \"theophilous\",\n    \"theryn\",\n    \"thobeka\",\n    \"thomas\",\n    \"thrash\",\n    \"tiana\",\n    \"tiera\",\n    \"tillman\",\n    \"tim\",\n    \"timmy\",\n    \"titania\",\n    \"tommy\",\n    \"toni\",\n    \"toomey\",\n    \"torus\",\n    \"tot\",\n    \"tourmaline\",\n    \"travis\",\n    \"trevino\",\n    \"trinity\",\n    \"tristin\",\n    \"truck\",\n    \"tucker\",\n    \"tuesday\",\n    \"tuxie\",\n    \"twofurious\",\n    \"tybal\",\n    \"tycho\",\n    \"tyler\",\n    \"tyreek\",\n    \"tyrese\",\n    \"ulrich\",\n    \"umi\",\n    \"una\",\n    \"ursula\",\n    \"usurper\",\n    \"utena\",\n    \"val\",\n    \"valentine\",\n    \"valueerror\",\n    \"vannevar\",\n    \"vasquez\",\n    \"velasquez\",\n    \"vernon\",\n    \"vero\",\n    \"veronica\",\n    \"vessalius\",\n    \"vidalia\",\n    \"vinathan\",\n    \"vinny\",\n    \"viola\",\n    \"violet\",\n    \"vipsanius\",\n    \"vito\",\n    \"vivian\",\n    \"wade\",\n    \"waffles\",\n    \"wall\",\n    \"wally\",\n    \"walton\",\n    \"wanda\",\n    \"washer\",\n    \"weeble\",\n    \"wei\",\n    \"wendy\",\n    \"wes\",\n    \"wesley\",\n    \"whimsy\",\n    \"whit\",\n    \"wichita\",\n    \"wiley\",\n    \"wilkerson\",\n    \"will\",\n    \"william\",\n    \"willow\",\n    \"wilma\",\n    \"wilson\",\n    \"winnie\",\n    \"workman\",\n    \"wyatt\",\n    \"xandra\",\n    \"ximena\",\n    \"xiu\",\n    \"yams\",\n    \"yanna\",\n    \"yasslyn\",\n    \"yazmin\",\n    \"yeong-ho\",\n    \"yong\",\n    \"york\",\n    \"yosh\",\n    \"yrjö\",\n    \"yulia\",\n    \"yummy\",\n    \"yurts\",\n    \"yusef\",\n    \"yusuf\",\n    \"zack\",\n    \"zaine\",\n    \"zane\",\n    \"zap\",\n    \"zeboriah\",\n    \"zee\",\n    \"zeke\",\n    \"zelda\",\n    \"zenzi\",\n    \"zephyr\",\n    \"zeruel\",\n    \"zesty\",\n    \"zi\",\n    \"zion\",\n    \"zippy\",\n    \"ziwa\",\n    \"zoey\",\n    \"zohaib\",\n    \"zutara\",\n];\n\npub const LAST_NAMES: &'static [&'static str] = &[\n    \"abbott\",\n    \"acevedo\",\n    \"adams\",\n    \"adamses\",\n    \"airport\",\n    \"alfredo\",\n    \"aliciakeyes\",\n    \"alighieri\",\n    \"almeida\",\n    \"alonzo\",\n    \"alstott\",\n    \"alvarado\",\n    \"ampersand\",\n    \"andante\",\n    \"anene\",\n    \"angry\",\n    \"anice\",\n    \"anteater\",\n    \"anthony\",\n    \"applesauce\",\n    \"aqualuft\",\n    \"arias\",\n    \"arkady\",\n    \"armstrong\",\n    \"ashby\",\n    \"ashwell\",\n    \"aster\",\n    \"atkinson\",\n    \"atomic\",\n    \"babatunde\",\n    \"bailey\",\n    \"baker\",\n    \"ball\",\n    \"ballard\",\n    \"ballson\",\n    \"balton\",\n    \"banananana\",\n    \"barajas\",\n    \"baresi\",\n    \"barios\",\n    \"bark\",\n    \"barker\",\n    \"barlow\",\n    \"barnes\",\n    \"baron\",\n    \"barrel\",\n    \"bartell\",\n    \"bartlette\",\n    \"baserunner\",\n    \"baskerville\",\n    \"basquez\",\n    \"bates\",\n    \"bathtub\",\n    \"batson\",\n    \"bean\",\n    \"beanbag\",\n    \"beanpot\",\n    \"beans\",\n    \"beard\",\n    \"beats\",\n    \"bedard\",\n    \"bedazzle\",\n    \"bedframe\",\n    \"beefsteak\",\n    \"beeks\",\n    \"belair\",\n    \"belfy\",\n    \"bellamy\",\n    \"bendie\",\n    \"benedicte\",\n    \"benitez\",\n    \"bentley\",\n    \"berger\",\n    \"bergeron\",\n    \"berrigan\",\n    \"best\",\n    \"beyonce\",\n    \"biancardi\",\n    \"biblioteca\",\n    \"bickle\",\n    \"biederman\",\n    \"bimblebottom\",\n    \"birdfather\",\n    \"birkenhagen\",\n    \"biscuits\",\n    \"bishop\",\n    \"bittercorn\",\n    \"blackburn\",\n    \"blacksmith\",\n    \"blanco\",\n    \"blaseseer\",\n    \"blaskets\",\n    \"blather\",\n    \"blomberg\",\n    \"blortles\",\n    \"blounder\",\n    \"blueberry\",\n    \"blueglass\",\n    \"bluesky\",\n    \"bluma\",\n    \"bobson\",\n    \"boingo\",\n    \"bondo\",\n    \"bong\",\n    \"bookbaby\",\n    \"boone\",\n    \"bootleg\",\n    \"borg\",\n    \"boston\",\n    \"bowen\",\n    \"bowers\",\n    \"boy\",\n    \"boyea\",\n    \"bradley\",\n    \"braga\",\n    \"breadwinner\",\n    \"briggs\",\n    \"bronx\",\n    \"brothers\",\n    \"brown\",\n    \"browning\",\n    \"buckley\",\n    \"buckridge\",\n    \"bugsnax\",\n    \"bullock\",\n    \"bundelle\",\n    \"bunion\",\n    \"buntoes\",\n    \"burgertoes\",\n    \"burkhard\",\n    \"burton\",\n    \"butt\",\n    \"buttercup\",\n    \"butts\",\n    \"byrd\",\n    \"byron\",\n    \"cabal\",\n    \"cain\",\n    \"calvino\",\n    \"camera\",\n    \"campbell\",\n    \"campos\",\n    \"canberra\",\n    \"candle\",\n    \"cantburn\",\n    \"capybara\",\n    \"caracal\",\n    \"carb\",\n    \"carberry\",\n    \"cardenas\",\n    \"carpenter\",\n    \"carver\",\n    \"cash\",\n    \"cashmoney\",\n    \"caster\",\n    \"castillo\",\n    \"catalina\",\n    \"catpashman\",\n    \"cave\",\n    \"ceilingfan\",\n    \"celestina\",\n    \"cena\",\n    \"cerna\",\n    \"cervantes\",\n    \"cerveza\",\n    \"chadwell\",\n    \"chamberlain\",\n    \"chamomile\",\n    \"chang\",\n    \"charcuterie\",\n    \"chark\",\n    \"chen\",\n    \"chi\",\n    \"chickadee\",\n    \"chickensalt\",\n    \"chill\",\n    \"chimes\",\n    \"chin\",\n    \"cholewinski\",\n    \"church\",\n    \"cilantro\",\n    \"cimino\",\n    \"clab\",\n    \"clambucket\",\n    \"clampner\",\n    \"clark\",\n    \"clembons\",\n    \"clemency\",\n    \"clutch\",\n    \"cobb\",\n    \"coleman\",\n    \"collins\",\n    \"colon\",\n    \"comas\",\n    \"combs\",\n    \"comeback\",\n    \"content\",\n    \"cookbook\",\n    \"coopwood\",\n    \"cornbread\",\n    \"correia\",\n    \"costa\",\n    \"cotterpin\",\n    \"cotton\",\n    \"crankit\",\n    \"crawford\",\n    \"cresthill\",\n    \"crikey\",\n    \"cross\",\n    \"crossing\",\n    \"crounse\",\n    \"crueller\",\n    \"crumb\",\n    \"crumpet\",\n    \"crunch\",\n    \"crutch\",\n    \"culler\",\n    \"cuthbert\",\n    \"cylinder\",\n    \"danger\",\n    \"darkness\",\n    \"datalake\",\n    \"davids\",\n    \"davis\",\n    \"day\",\n    \"debaskervilles\",\n    \"demarzen\",\n    \"desheilds\",\n    \"deshields\",\n    \"dean\",\n    \"decksetter\",\n    \"delacruz\",\n    \"delaney\",\n    \"deleuze\",\n    \"dembélé\",\n    \"denardi\",\n    \"denman\",\n    \"dennis\",\n    \"destiny\",\n    \"dewey\",\n    \"di batterino\",\n    \"diaz\",\n    \"dice\",\n    \"dickerson\",\n    \"doctor\",\n    \"dogwalker\",\n    \"dollie\",\n    \"donaldson\",\n    \"dosime\",\n    \"dotcom\",\n    \"dougnut\",\n    \"dovenpart\",\n    \"doyle\",\n    \"dracaena\",\n    \"drama\",\n    \"draper\",\n    \"dreamy\",\n    \"drennan\",\n    \"drobot\",\n    \"droodle\",\n    \"drumsolo\",\n    \"dry\",\n    \"duckdinner\",\n    \"dudley\",\n    \"duduk\",\n    \"duende\",\n    \"duffy\",\n    \"duggins\",\n    \"dumpington\",\n    \"dunno\",\n    \"duo\",\n    \"duodenum\",\n    \"duran\",\n    \"durango\",\n    \"duress\",\n    \"duvill\",\n    \"easterbrook\",\n    \"eberhardt\",\n    \"eckhardt\",\n    \"edwards\",\n    \"eggburt\",\n    \"eggleton\",\n    \"eigengrau\",\n    \"elemefayo\",\n    \"elftower\",\n    \"elliott\",\n    \"ender\",\n    \"england\",\n    \"english\",\n    \"enjoyable\",\n    \"erock\",\n    \"escobar\",\n    \"espinoza\",\n    \"estes\",\n    \"evergreen\",\n    \"facepunch\",\n    \"fairwood\",\n    \"falconer\",\n    \"familia\",\n    \"fantastic\",\n    \"fardo\",\n    \"fashion\",\n    \"feather\",\n    \"fenestrate\",\n    \"ferguson\",\n    \"ferraro\",\n    \"fiasco\",\n    \"fiesta\",\n    \"fig\",\n    \"fightcastle\",\n    \"figueroa\",\n    \"findlay\",\n    \"fingerguns\",\n    \"firestar\",\n    \"firestone\",\n    \"firewall\",\n    \"fischer\",\n    \"flahwah\",\n    \"fledermaus\",\n    \"flemming\",\n    \"flex\",\n    \"flores\",\n    \"flum\",\n    \"foamcore\",\n    \"foible\",\n    \"forbes\",\n    \"fougere\",\n    \"fouqet\",\n    \"fox\",\n    \"francobollo\",\n    \"frank\",\n    \"franklin\",\n    \"frederick\",\n    \"freed\",\n    \"freeman\",\n    \"friday\",\n    \"friedrich\",\n    \"friendo\",\n    \"frihart\",\n    \"fring\",\n    \"frost\",\n    \"frosting\",\n    \"frumple\",\n    \"furnace\",\n    \"gagnon\",\n    \"gallant\",\n    \"galley\",\n    \"galvanic\",\n    \"games\",\n    \"garbage\",\n    \"garcia\",\n    \"garner\",\n    \"gawrsh\",\n    \"george\",\n    \"gesundheit\",\n    \"ghighi\",\n    \"giant\",\n    \"gibas\",\n    \"gigstad\",\n    \"gildehaus\",\n    \"givens\",\n    \"givewell\",\n    \"glass\",\n    \"gleiss\",\n    \"gloom\",\n    \"glover\",\n    \"glump\",\n    \"goblin\",\n    \"goedecke\",\n    \"golightly\",\n    \"gonzales\",\n    \"gonzalez\",\n    \"goo\",\n    \"good\",\n    \"goodhart\",\n    \"gooseball\",\n    \"gooseberry\",\n    \"gorczyca\",\n    \"gorge\",\n    \"grackle\",\n    \"grassly\",\n    \"greatness\",\n    \"greenlemon\",\n    \"griffin\",\n    \"griffith\",\n    \"gritt\",\n    \"groberg\",\n    \"groblonx\",\n    \"grocer\",\n    \"gubbins\",\n    \"guerra\",\n    \"guerreiro\",\n    \"gulp\",\n    \"guzman\",\n    \"gwiffin\",\n    \"haddad\",\n    \"hairston\",\n    \"haley\",\n    \"halifax\",\n    \"hambone\",\n    \"hambright\",\n    \"hamburger\",\n    \"hammer\",\n    \"hardaway\",\n    \"harding\",\n    \"hardison\",\n    \"harper\",\n    \"harrell\",\n    \"harrington\",\n    \"harrison\",\n    \"harrow\",\n    \"harvey\",\n    \"harvie\",\n    \"hatchler\",\n    \"haunt\",\n    \"hayes\",\n    \"haynes\",\n    \"haza\",\n    \"heartlight\",\n    \"heat\",\n    \"henderson\",\n    \"hendler\",\n    \"hendricks\",\n    \"henriques\",\n    \"herman\",\n    \"hernandez\",\n    \"herrold\",\n    \"hess\",\n    \"highlife\",\n    \"highway\",\n    \"hildebert\",\n    \"hirsch\",\n    \"hitherto\",\n    \"hobbity\",\n    \"hockeypuck\",\n    \"hojo\",\n    \"holbrook\",\n    \"holloway\",\n    \"hollywood\",\n    \"homestyle\",\n    \"honey\",\n    \"honeywell\",\n    \"hookrace\",\n    \"horne\",\n    \"horseman\",\n    \"hotdogfingers\",\n    \"houndlog\",\n    \"howard\",\n    \"howe\",\n    \"hu\",\n    \"hubet\",\n    \"hubette\",\n    \"huerta\",\n    \"huhtala\",\n    \"humdinger\",\n    \"hunter\",\n    \"hyperpop\",\n    \"immenga\",\n    \"inagame\",\n    \"incarnate\",\n    \"ingram\",\n    \"innamorato\",\n    \"inningson\",\n    \"internet\",\n    \"irby\",\n    \"isarobot\",\n    \"italodisco\",\n    \"ito\",\n    \"izquierda\",\n    \"jackson\",\n    \"james\",\n    \"javier\",\n    \"jaylee\",\n    \"jeff\",\n    \"jeggings\",\n    \"jensen\",\n    \"jesaulenko\",\n    \"jespersen\",\n    \"ji\",\n    \"ji-eun\",\n    \"johnson\",\n    \"jokes\",\n    \"jonbois\",\n    \"jones\",\n    \"judochop\",\n    \"junebug\",\n    \"junior jr\",\n    \"kalette\",\n    \"kane\",\n    \"kappen jr.\",\n    \"karim\",\n    \"kath\",\n    \"kehl\",\n    \"kelp\",\n    \"keming\",\n    \"kendrick\",\n    \"kennedy\",\n    \"kenny\",\n    \"kensington\",\n    \"kerfuffle\",\n    \"kerwin\",\n    \"kesh\",\n    \"keyes\",\n    \"kiddo\",\n    \"kiebala\",\n    \"kim\",\n    \"kimball\",\n    \"king\",\n    \"kingbird\",\n    \"kirby\",\n    \"kirchner\",\n    \"kisselburg\",\n    \"klaich\",\n    \"knuckles\",\n    \"koch\",\n    \"koeppe\",\n    \"konderla\",\n    \"koning\",\n    \"konk\",\n    \"kramer\",\n    \"kranch\",\n    \"kravitz\",\n    \"krill\",\n    \"kropotkin\",\n    \"krueger\",\n    \"ksipra\",\n    \"kugel\",\n    \"kyser\",\n    \"labelle\",\n    \"laabs\",\n    \"ladd\",\n    \"ladrona\",\n    \"lamani\",\n    \"lampman\",\n    \"lancaster\",\n    \"langzone\",\n    \"lanyard\",\n    \"laplace\",\n    \"larsen\",\n    \"lascu\",\n    \"latch\",\n    \"latenight\",\n    \"latke\",\n    \"lauer\",\n    \"laurie\",\n    \"lawson\",\n    \"lazarus\",\n    \"leblanc\",\n    \"lemath\",\n    \"leaf\",\n    \"leal\",\n    \"leatherman\",\n    \"lee\",\n    \"leeks\",\n    \"lemma\",\n    \"lenny\",\n    \"li\",\n    \"lightner\",\n    \"lin\",\n    \"linard\",\n    \"lincecum\",\n    \"lingardo\",\n    \"liu\",\n    \"logan\",\n    \"lompa\",\n    \"longarms\",\n    \"loofah\",\n    \"lopez\",\n    \"loser\",\n    \"lott\",\n    \"lotte\",\n    \"lotus\",\n    \"loveless\",\n    \"lozano\",\n    \"lutefisk\",\n    \"mac\",\n    \"macintosh\",\n    \"maclear\",\n    \"madrigal\",\n    \"mae\",\n    \"magpie\",\n    \"mahle\",\n    \"makin\",\n    \"malackey\",\n    \"maldonado\",\n    \"mallow\",\n    \"manco\",\n    \"mandible\",\n    \"mango\",\n    \"manhattan\",\n    \"mantilla\",\n    \"marama\",\n    \"marijuana\",\n    \"marlow\",\n    \"marovic\",\n    \"marsh\",\n    \"marshallow\",\n    \"marzen\",\n    \"mason\",\n    \"massey\",\n    \"mathews\",\n    \"matos\",\n    \"matsuyama\",\n    \"matte\",\n    \"mauser\",\n    \"maybane\",\n    \"maybe\",\n    \"mayonnaise\",\n    \"mcblase\",\n    \"mccloud\",\n    \"mccoy\",\n    \"mcelroy\",\n    \"mcg\",\n    \"mcghee\",\n    \"mcgill\",\n    \"mcgribbits\",\n    \"mckinley\",\n    \"mcsriff\",\n    \"mccall\",\n    \"mcdaniel\",\n    \"meadows\",\n    \"meatbrick\",\n    \"meh\",\n    \"melcon\",\n    \"melgoza\",\n    \"melo\",\n    \"melon\",\n    \"melton\",\n    \"mendizza\",\n    \"mendoza\",\n    \"meng\",\n    \"merritt\",\n    \"metzger\",\n    \"michelotti\",\n    \"michet\",\n    \"midcentury\",\n    \"middlebrook\",\n    \"milicic\",\n    \"mina\",\n    \"mininger\",\n    \"miran\",\n    \"mist\",\n    \"mitchell\",\n    \"mocha\",\n    \"mondale\",\n    \"mondegreen\",\n    \"monreal\",\n    \"monstera\",\n    \"monteiro\",\n    \"moodley\",\n    \"moon\",\n    \"mora\",\n    \"moran\",\n    \"moreno\",\n    \"morin\",\n    \"morse\",\n    \"moss\",\n    \"mueller\",\n    \"muggins\",\n    \"murphy\",\n    \"mustard\",\n    \"myers\",\n    \"nakamoto\",\n    \"nakamura\",\n    \"nameperson\",\n    \"nanda\",\n    \"narismulu\",\n    \"nash\",\n    \"nattee\",\n    \"nava\",\n    \"nelson\",\n    \"neske\",\n    \"nettle\",\n    \"ng\",\n    \"ngozi\",\n    \"nibb\",\n    \"nightmare\",\n    \"nocturne\",\n    \"nolan\",\n    \"nopales\",\n    \"norindr\",\n    \"noscope\",\n    \"notarobot\",\n    \"novak\",\n    \"nugget\",\n    \"nyeos\",\n    \"nyong'o\",\n    \"o'brian\",\n    \"o'lantern\",\n    \"object\",\n    \"obrien\",\n    \"oconnor\",\n    \"octothorp\",\n    \"oki\",\n    \"oko\",\n    \"olive\",\n    \"oliveira\",\n    \"omelette\",\n    \"osborn\",\n    \"otherman\",\n    \"otten\",\n    \"outlaw\",\n    \"overbey\",\n    \"owens\",\n    \"owlbears\",\n    \"o’houlihan\",\n    \"pace\",\n    \"pacheco\",\n    \"paider\",\n    \"paint\",\n    \"painter\",\n    \"palladium\",\n    \"pamplin\",\n    \"pancakes\",\n    \"pantheocide\",\n    \"park\",\n    \"parra\",\n    \"passon\",\n    \"pasta\",\n    \"patchwork\",\n    \"pate\",\n    \"patterson\",\n    \"payment\",\n    \"peacelily\",\n    \"pebble\",\n    \"peck\",\n    \"peep\",\n    \"peeps\",\n    \"pelagos\",\n    \"peperomioides\",\n    \"pepperdile\",\n    \"perez\",\n    \"permadeath\",\n    \"peterson\",\n    \"petty\",\n    \"piazza\",\n    \"picklestein\",\n    \"pinceau\",\n    \"pingleton\",\n    \"pink\",\n    \"pleck\",\n    \"pliskin\",\n    \"plums\",\n    \"po\",\n    \"podcast\",\n    \"pony\",\n    \"poole\",\n    \"portmanteau\",\n    \"potatorade\",\n    \"pothos\",\n    \"powers\",\n    \"preisendorf\",\n    \"prestige\",\n    \"preston\",\n    \"prettygood\",\n    \"prowler\",\n    \"pruessner\",\n    \"puddles\",\n    \"pynchon\",\n    \"quartz\",\n    \"quimby\",\n    \"quitter\",\n    \"ra\",\n    \"rambutan\",\n    \"ramos\",\n    \"ramsey\",\n    \"rangel\",\n    \"rascal\",\n    \"ratoon\",\n    \"reddick\",\n    \"redlight\",\n    \"redox\",\n    \"reeves\",\n    \"relish\",\n    \"ren\",\n    \"rice\",\n    \"richardson\",\n    \"rincón\",\n    \"ringmaster\",\n    \"risset\",\n    \"rivera\",\n    \"roadhouse\",\n    \"robbie\",\n    \"robins\",\n    \"robot\",\n    \"robotnivic\",\n    \"rocha\",\n    \"roche\",\n    \"rochester\",\n    \"rodgers\",\n    \"rodriguez\",\n    \"rogers\",\n    \"roland\",\n    \"rolsenthal\",\n    \"romayne\",\n    \"ronero\",\n    \"ronzoni\",\n    \"root\",\n    \"rosa\",\n    \"rosales\",\n    \"roseheart\",\n    \"ross\",\n    \"rotato\",\n    \"rounder\",\n    \"rubberbat\",\n    \"rubberman\",\n    \"rugrat\",\n    \"ruiz\",\n    \"rush\",\n    \"ruth\",\n    \"rutledge\",\n    \"ryman\",\n    \"saathoff\",\n    \"saetang\",\n    \"safari\",\n    \"sagaba\",\n    \"salad\",\n    \"salt\",\n    \"sanchez\",\n    \"sanders\",\n    \"sands\",\n    \"santana\",\n    \"santiago\",\n    \"sasquatch\",\n    \"sato\",\n    \"scandal\",\n    \"scantron\",\n    \"schenn\",\n    \"schiefer\",\n    \"schmitt\",\n    \"schofield\",\n    \"schumacher\",\n    \"scolopax\",\n    \"scoresburg\",\n    \"scorpler\",\n    \"scotch\",\n    \"scott\",\n    \"scrobbles\",\n    \"scrollbar\",\n    \"scuttlebug\",\n    \"seabright\",\n    \"seagull\",\n    \"seasalt\",\n    \"sedillo\",\n    \"seeth\",\n    \"segee\",\n    \"selach\",\n    \"semiquaver\",\n    \"septemberish\",\n    \"seraph\",\n    \"serotonin\",\n    \"sharpe\",\n    \"shelton\",\n    \"shmurmgle\",\n    \"short\",\n    \"shortvat\",\n    \"shotwell\",\n    \"shriffle\",\n    \"shufflecat\",\n    \"shupe\",\n    \"sierpinski\",\n    \"silk\",\n    \"simmons\",\n    \"simpson\",\n    \"skagerrak\",\n    \"skitter\",\n    \"sky\",\n    \"slice\",\n    \"sliders\",\n    \"slugger\",\n    \"slumps\",\n    \"slurms\",\n    \"smaht\",\n    \"small\",\n    \"smith\",\n    \"snail\",\n    \"snapjaw\",\n    \"snart\",\n    \"snodgrass\",\n    \"snyder\",\n    \"soares\",\n    \"sobremesa\",\n    \"sokol\",\n    \"solis\",\n    \"solo\",\n    \"song\",\n    \"soto\",\n    \"soul\",\n    \"soun\",\n    \"sounders\",\n    \"southwick\",\n    \"spaceman\",\n    \"sparks\",\n    \"sparrow\",\n    \"speedrun\",\n    \"spheroid\",\n    \"spieth\",\n    \"splendor\",\n    \"spliff\",\n    \"splotter\",\n    \"spoon\",\n    \"sports\",\n    \"sportsman\",\n    \"spruce\",\n    \"squall\",\n    \"squantorini\",\n    \"standlake\",\n    \"stanton\",\n    \"star\",\n    \"starling\",\n    \"statter\",\n    \"statter jr.\",\n    \"steakknife\",\n    \"steeplechase\",\n    \"stegmann\",\n    \"stickybeak\",\n    \"stink\",\n    \"stompman\",\n    \"strawberry\",\n    \"street\",\n    \"strewnberry\",\n    \"strife\",\n    \"stringlight\",\n    \"stromboli\",\n    \"strongbody\",\n    \"succotash\",\n    \"suljak\",\n    \"summer\",\n    \"sun\",\n    \"sundae\",\n    \"sunkcost\",\n    \"sunset\",\n    \"sunshine\",\n    \"suplex\",\n    \"sutherland\",\n    \"suzanne\",\n    \"suzuki\",\n    \"swagger\",\n    \"swain\",\n    \"swan\",\n    \"swandre\",\n    \"swank\",\n    \"swift\",\n    \"swine\",\n    \"swinger\",\n    \"synomyn\",\n    \"tabby\",\n    \"tables\",\n    \"tails\",\n    \"takahashi\",\n    \"tanaka\",\n    \"tankris\",\n    \"tasmin\",\n    \"taswell\",\n    \"tattersall\",\n    \"taylor\",\n    \"teixeira\",\n    \"telephone\",\n    \"tenderson\",\n    \"tenley\",\n    \"terermorphasis\",\n    \"thane\",\n    \"thibault\",\n    \"thompson\",\n    \"threetimes\",\n    \"throck\",\n    \"throckmorton\",\n    \"throg\",\n    \"throgmorten\",\n    \"thwompson\",\n    \"toast\",\n    \"toaster\",\n    \"tokkan\",\n    \"tolleson\",\n    \"tooke\",\n    \"toothcake\",\n    \"torres\",\n    \"tosser\",\n    \"towns\",\n    \"townsend\",\n    \"tratnyek\",\n    \"treadstone\",\n    \"tredwell\",\n    \"trefeather\",\n    \"triumphant\",\n    \"trololol\",\n    \"trombone\",\n    \"truk\",\n    \"tugboat\",\n    \"tumblehome\",\n    \"turner\",\n    \"turnip\",\n    \"turquoise\",\n    \"twelve\",\n    \"ultrabass\",\n    \"underbuck\",\n    \"uniondues\",\n    \"uppercutski\",\n    \"ups\",\n    \"ursino\",\n    \"vainglory\",\n    \"valadez\",\n    \"valenzuela\",\n    \"vandermale\",\n    \"vanstrander\",\n    \"vapor\",\n    \"vargas\",\n    \"vaughan\",\n    \"velazquez\",\n    \"vesperidian\",\n    \"vincent\",\n    \"vine\",\n    \"viney\",\n    \"violence\",\n    \"violet\",\n    \"vodka\",\n    \"voorhees\",\n    \"wagg\",\n    \"walker\",\n    \"wallace\",\n    \"wallop\",\n    \"walton\",\n    \"wan\",\n    \"wanda\",\n    \"wanderlust\",\n    \"warhorse\",\n    \"warren\",\n    \"washington\",\n    \"watson\",\n    \"watts\",\n    \"weatherman\",\n    \"weeks\",\n    \"weir\",\n    \"wells\",\n    \"wetchup\",\n    \"whammy\",\n    \"wheeler\",\n    \"wheerer\",\n    \"whelp\",\n    \"whiskey\",\n    \"whitney\",\n    \"wigdoubt\",\n    \"wilcox\",\n    \"wildarms\",\n    \"williams\",\n    \"willow\",\n    \"willowtree\",\n    \"wilson\",\n    \"winfield\",\n    \"winkler\",\n    \"winner\",\n    \"winter\",\n    \"winters\",\n    \"wise\",\n    \"wobin\",\n    \"woman\",\n    \"wood\",\n    \"woodman\",\n    \"woods\",\n    \"wooly\",\n    \"woomy\",\n    \"wooten\",\n    \"wormthrice\",\n    \"wright\",\n    \"wuppo\",\n    \"wyeth\",\n    \"xu\",\n    \"yaboi\",\n    \"yamamoto\",\n    \"yamashita\",\n    \"yardstick\",\n    \"yarrum\",\n    \"yesterday\",\n    \"yolk\",\n    \"youngblood\",\n    \"yuniesky\",\n    \"zavala\",\n    \"zeagler\",\n    \"zenith\",\n    \"zephyr\",\n    \"zhao\",\n    \"zheng\",\n    \"zhivago\",\n    \"zhuge\",\n    \"zimmerman\",\n    \"zonker\",\n    \"zoobrambana\",\n    \"de-vos\",\n];\n"
  },
  {
    "path": "lib/rotbart/src/elfs.rs",
    "content": "pub const ADJECTIVES: &'static [&'static str] = &[\n    \"able\", \"abnorma\", \"again\", \"airexpl\", \"ang\", \"anger\", \"asail\", \"attack\", \"aurora\", \"awl\",\n    \"ban\", \"band\", \"bare\", \"beat\", \"beated\", \"belly\", \"bind\", \"bite\", \"bloc\", \"blood\", \"body\",\n    \"book\", \"breath\", \"bump\", \"cast\", \"cham\", \"clamp\", \"clap\", \"claw\", \"clear\", \"cli\", \"clip\",\n    \"cloud\", \"contro\", \"convy\", \"coolhit\", \"crash\", \"cry\", \"cut\", \"descri\", \"d-fight\", \"dig\",\n    \"ditch\", \"div\", \"doz\", \"dre\", \"dul\", \"du-pin\", \"dye\", \"earth\", \"edu\", \"eg-bomb\", \"egg\",\n    \"elegy\", \"ele-hit\", \"embody\", \"empli\", \"engl\", \"erupt\", \"evens\", \"explor\", \"eyes\", \"fall\",\n    \"fast\", \"f-car\", \"f-dance\", \"fears\", \"f-fight\", \"fight\", \"fir\", \"fire\", \"firehit\", \"flame\",\n    \"flap\", \"flash\", \"flew\", \"force\", \"fra\", \"freeze\", \"frog\", \"g-bird\", \"genkiss\", \"gift\",\n    \"g-kiss\", \"g-mouse\", \"grade\", \"grow\", \"hammer\", \"hard\", \"hat\", \"hate\", \"h-bomb\", \"hell-r\",\n    \"hemp\", \"hint\", \"hit\", \"hu\", \"hunt\", \"hypnosi\", \"inha\", \"iro\", \"ironbar\", \"ir-wing\", \"j-gun\",\n    \"kee\", \"kick\", \"knif\", \"knife\", \"knock\", \"level\", \"ligh\", \"lighhit\", \"light\", \"live\", \"l-wall\",\n    \"mad\", \"majus\", \"mel\", \"melo\", \"mess\", \"milk\", \"mimi\", \"miss\", \"mixing\", \"move\", \"mud\",\n    \"ni-bed\", \"noisy\", \"noonli\", \"null\", \"n-wave\", \"pat\", \"peace\", \"pin\", \"plan\", \"plane\", \"pois\",\n    \"pol\", \"powde\", \"powe\", \"power\", \"prize\", \"protect\", \"proud\", \"rage\", \"recor\", \"reflac\",\n    \"refrec\", \"regr\", \"reliv\", \"renew\", \"r-fight\", \"ring\", \"rkick\", \"rock\", \"round\", \"rus\", \"rush\",\n    \"sand\", \"saw\", \"scissor\", \"scra\", \"script\", \"seen\", \"server\", \"shadow\", \"shell\", \"shine\",\n    \"sho\", \"sight\", \"sin\", \"small\", \"smelt\", \"smok\", \"snake\", \"sno\", \"snow\", \"sou\", \"so-wave\",\n    \"spar\", \"spec\", \"spid\", \"s-pin\", \"spra\", \"stam\", \"stare\", \"stea\", \"stone\", \"storm\", \"stru\",\n    \"strug\", \"studen\", \"subs\", \"sucid\", \"sun-lig\", \"sunris\", \"suply\", \"s-wave\", \"tails\", \"tangl\",\n    \"taste\", \"telli\", \"thank\", \"tonkick\", \"tooth\", \"torl\", \"train\", \"trikick\", \"tunge\", \"volt\",\n    \"wa-gun\", \"watch\", \"wave\", \"w-bomb\", \"wfall\", \"wfing\", \"whip\", \"whirl\", \"wind\", \"wolf\", \"wood\",\n    \"wor\", \"yuja\",\n];\n\npub const NOUNS: &'static [&'static str] = &[\n    \"seed\", \"grass\", \"flowe\", \"shad\", \"cabr\", \"snake\", \"gold\", \"cow\", \"guiki\", \"pedal\", \"delan\",\n    \"b-fly\", \"bide\", \"keyu\", \"fork\", \"lap\", \"pige\", \"pijia\", \"caml\", \"lat\", \"bird\", \"baboo\", \"viv\",\n    \"aboke\", \"pikaq\", \"rye\", \"san\", \"bread\", \"lidel\", \"lide\", \"pip\", \"pikex\", \"rok\", \"jugen\",\n    \"pud\", \"bude\", \"zhib\", \"gelu\", \"gras\", \"flow\", \"laful\", \"ath\", \"bala\", \"corn\", \"moluf\", \"desp\",\n    \"daked\", \"mimi\", \"bolux\", \"koda\", \"gelud\", \"monk\", \"sumoy\", \"gedi\", \"wendi\", \"nilem\", \"nile\",\n    \"nilec\", \"kezi\", \"yongl\", \"hude\", \"wanli\", \"geli\", \"guail\", \"madaq\", \"wuci\", \"wuci\", \"mujef\",\n    \"jelly\", \"sicib\", \"gelu\", \"neluo\", \"boli\", \"jiale\", \"yed\", \"yede\", \"clo\", \"scare\", \"aoco\",\n    \"dede\", \"dedei\", \"bawu\", \"jiug\", \"badeb\", \"badeb\", \"hole\", \"balux\", \"ges\", \"fant\", \"quar\",\n    \"yihe\", \"swab\", \"slipp\", \"clu\", \"depos\", \"biliy\", \"yuano\", \"some\", \"no\", \"yela\", \"empt\",\n    \"zecun\", \"xiahe\", \"bolel\", \"deji\", \"macid\", \"xihon\", \"xito\", \"luck\", \"menji\", \"gelu\", \"deci\",\n    \"xide\", \"dasaj\", \"dongn\", \"ricul\", \"minxi\", \"baliy\", \"zenda\", \"luzel\", \"hele5\", \"0fenb\",\n    \"kail\", \"jiand\", \"carp\", \"jinde\", \"lapu\", \"mude\", \"yifu\", \"linli\", \"sandi\", \"husi\", \"jinc\",\n    \"oumu\", \"oumux\", \"cap\", \"kuiza\", \"pud\", \"tiao\", \"frman\", \"clau\", \"spark\", \"drago\", \"boliu\",\n    \"guail\", \"miyou\", \"miy\", \"qiaok\", \"beil\", \"mukei\", \"rided\", \"madam\", \"bagep\", \"croc\", \"alige\",\n    \"oudal\", \"oud\", \"dada\", \"hehe\", \"yedea\", \"nuxi\", \"nuxin\", \"rouy\", \"aliad\", \"stick\", \"qiang\",\n    \"laand\", \"piqi\", \"pi\", \"pupi\", \"deke\", \"dekej\", \"nadi\", \"nadio\", \"mali\", \"pea\", \"elect\",\n    \"flowe\", \"mal\", \"mali\", \"hushu\", \"nilee\", \"yuzi\", \"popoz\", \"duzi\", \"heba\", \"xian\", \"shan\",\n    \"yeyea\", \"wuy\", \"luo\", \"kefe\", \"hula\", \"crow\", \"yadeh\", \"mow\", \"annan\", \"suoni\", \"kyli\",\n    \"hulu\", \"hudel\", \"yehe\", \"gulae\", \"yehe\", \"blu\", \"gelan\", \"boat\", \"nip\", \"poit\", \"helak\",\n    \"xinl\", \"bear\", \"linb\", \"mageh\", \"magej\", \"wuli\", \"yide\", \"rive\", \"fish\", \"aogu\", \"delie\",\n    \"mante\", \"konmu\", \"delu\", \"helu\", \"huan\", \"huma\", \"dongf\", \"jinca\", \"hede\", \"defu\", \"liby\",\n    \"jiapa\", \"meji\", \"hele\", \"buhu\", \"milk\", \"habi\", \"thun\", \"gard\", \"don\", \"yangq\", \"sanaq\",\n    \"banq\", \"luj\", \"phix\", \"siei\", \"egg\",\n];\n"
  },
  {
    "path": "lib/rotbart/src/lib.rs",
    "content": "mod blaseball;\nmod elfs;\nmod mlp_fim;\nmod pokemon;\nmod xc1;\nmod xc2;\n\nlazy_static::lazy_static! {\n    pub static ref COMBINED_ADJ: Vec<&'static str> = {\n        let mut adjs: Vec<&str> = vec![];\n        adjs.extend(xc1::ADJECTIVES.iter());\n        adjs.extend(xc2::ADJECTIVES.iter());\n        adjs.extend(elfs::ADJECTIVES.iter());\n        adjs.extend(blaseball::FIRST_NAMES.iter());\n        adjs.sort();\n        adjs.dedup();\n        adjs\n    };\n\n    pub static ref COMBINED_NOUN: Vec<&'static str> = {\n        let mut nouns: Vec<&str> = vec![];\n        nouns.extend(xc1::NOUNS.iter());\n        nouns.extend(xc2::NOUNS.iter());\n        nouns.extend(xc2::COMMON_BLADES.iter());\n        nouns.extend(elfs::NOUNS.iter());\n        nouns.extend(mlp_fim::PONIES.iter());\n        nouns.extend(pokemon::POKEDEX.iter());\n        nouns.extend(blaseball::LAST_NAMES.iter());\n        nouns.sort();\n        nouns.dedup();\n        nouns\n    };\n}\n\npub fn unique_monster() -> Option<String> {\n    let mut generator = names::Generator::new(&COMBINED_ADJ, &COMBINED_NOUN, names::Name::Plain);\n    generator.next()\n}\n"
  },
  {
    "path": "lib/rotbart/src/mlp_fim.rs",
    "content": "pub const PONIES: &'static [&'static str] = &[\n    // earth ponies\n    \"applejack\",\n    \"pinkie-pie\",\n    \"aloe\",\n    \"butternut\",\n    \"cheerilee\",\n    \"coloratura\",\n    \"dr-fauna\",\n    \"junebug\",\n    \"lighthoof\",\n    \"mane-iac\",\n    \"roma\",\n    \"torch-song\",\n    \"wrangler\",\n    \"zesty\",\n    \"big-bucks\",\n    \"braeburn\",\n    \"burnt-oak\",\n    \"cattail\",\n    \"code-red\",\n    \"dr-horse\",\n    \"gizmo\",\n    \"gladmane\",\n    \"hard-hat\",\n    \"mr-stripes\",\n    \"mudbriar\",\n    \"oak-nut\",\n    \"rockhoof\",\n    \"sans-smirk\",\n    \"starstreak\",\n    \"svengallop\",\n    \"toe-tapper\",\n    \"twisty-pop\",\n    \"apple-top\",\n    \"jonagold\",\n    \"magdalena\",\n    \"red-gala\",\n    \"sundowner\",\n    \"apple-core\",\n    \"bushel\",\n    \"wensley\",\n    \"avalon\",\n    \"bell-perin\",\n    \"belle-star\",\n    \"berryshine\",\n    \"betty-hoof\",\n    \"blue-bows\",\n    \"blue-cutie\",\n    \"blue-nile\",\n    \"bonnie\",\n    \"bottlecap\",\n    \"butter-pop\",\n    \"candy-mane\",\n    \"carlotta\",\n    \"cascada\",\n    \"charged-up\",\n    \"cornflower\",\n    \"crescendo\",\n    \"creamcup\",\n    \"cultivar\",\n    \"daisy\",\n    \"doseydotes\",\n    \"dry-wheat\",\n    \"floral-pan\",\n    \"flounder\",\n    \"flurry\",\n    \"frou-frou\",\n    \"hoda-kotb\",\n    \"honey-dew\",\n    \"jinx\",\n    \"jorunn\",\n    \"jubileena\",\n    \"lady-gaval\",\n    \"little-po\",\n    \"long-shot\",\n    \"luckette\",\n    \"lucky-star\",\n    \"majesty\",\n    \"maribelle\",\n    \"maybelline\",\n    \"meadowluck\",\n    \"millie\",\n    \"mint-swirl\",\n    \"minty\",\n    \"mjolna\",\n    \"oakey-doke\",\n    \"obscurity\",\n    \"offbeat\",\n    \"penny-ante\",\n    \"play-write\",\n    \"rogue-ruby\",\n    \"rose\",\n    \"rosemary\",\n    \"rosetta\",\n    \"roxie-rave\",\n    \"screwball\",\n    \"screwy\",\n    \"seasong\",\n    \"serena\",\n    \"shoeshine\",\n    \"sky-view\",\n    \"soft-spot\",\n    \"soot-stain\",\n    \"sun-streak\",\n    \"sunfire\",\n    \"surf\",\n    \"sweetberry\",\n    \"tarantella\",\n    \"toffee\",\n    \"tough-love\",\n    \"tree-sap\",\n    \"viola\",\n    \"welly\",\n    \"yuma-spurs\",\n    \"zen-moment\",\n    \"ace-point\",\n    \"adante\",\n    \"affero\",\n    \"al-roker\",\n    \"b-sharp\",\n    \"baritone\",\n    \"beuford\",\n    \"big-top\",\n    \"mr-breezy\",\n    \"caboose\",\n    \"comb-over\",\n    \"cormano\",\n    \"davenport\",\n    \"dirtbound\",\n    \"don-neigh\",\n    \"dr-hooves\",\n    \"eiffel\",\n    \"end-zone\",\n    \"felix\",\n    \"free-throw\",\n    \"funnel-web\",\n    \"full-steam\",\n    \"hay-fever\",\n    \"heisenbuck\",\n    \"hercules\",\n    \"icy-drop\",\n    \"iron-bark\",\n    \"jim-beam\",\n    \"john-bull\",\n    \"kazooie\",\n    \"klein\",\n    \"leadwing\",\n    \"levon-song\",\n    \"lincoln\",\n    \"matt-lauer\",\n    \"mccree\",\n    \"melilot\",\n    \"noteworthy\",\n    \"opulence\",\n    \"pink-drink\",\n    \"ragtime\",\n    \"rivet\",\n    \"royal-riff\",\n    \"shamrock\",\n    \"shiny-pear\",\n    \"shortround\",\n    \"smokestack\",\n    \"sour-drops\",\n    \"sourpuss\",\n    \"star-gazer\",\n    \"tall-order\",\n    \"tall-tale\",\n    \"two-ton\",\n    \"vegemite\",\n    \"wetzel\",\n    \"wisp\",\n    \"mr-zippy\",\n    \"apple-rose\",\n    \"aquamarine\",\n    \"charm\",\n    \"derpy\",\n    \"drizzle\",\n    \"fine-line\",\n    \"fluttershy\",\n    \"helia\",\n    \"lemony-gem\",\n    \"little-red\",\n    \"merry-may\",\n    \"minuette\",\n    \"parasol\",\n    \"peach-fuzz\",\n    \"rarity\",\n    \"sassaflash\",\n    \"scootaloo\",\n    \"sea-swirl\",\n    \"swan-song\",\n    \"twist\",\n    \"yona\",\n    \"comet-tail\",\n    \"cosmic\",\n    \"first-base\",\n    \"grand-pear\",\n    \"log-jam\",\n    \"mane-moon\",\n    \"rare-find\",\n    \"sand-trap\",\n    \"sandbar\",\n    \"starburst\",\n    \"thorn\",\n    \"mr-waddle\",\n    \"warm-front\",\n    \"whiplash\",\n    // pegasi\n    \"fluttershy\",\n    \"buttershy\",\n    \"daring-do\",\n    \"flitter\",\n    \"hoops\",\n    \"inky-rose\",\n    \"open-skies\",\n    \"snowdash\",\n    \"somnambula\",\n    \"sunshower\",\n    \"fleetfoot\",\n    \"soarin\",\n    \"spitfire\",\n    \"blaze\",\n    \"high-winds\",\n    \"misty-fly\",\n    \"sun-chaser\",\n    \"surprise\",\n    \"wave-chill\",\n    \"wind-waker\",\n    \"wind-rider\",\n    \"fast-clip\",\n    \"tight-ship\",\n    \"whiplash\",\n    \"icy-rain\",\n    \"lime-jelly\",\n    \"parasol\",\n    \"sassaflash\",\n    \"sightseer\",\n    \"starburst\",\n    \"thorn\",\n    \"warm-front\",\n    \"whitewash\",\n    \"wild-fire\",\n    \"aqua-burst\",\n    \"big-bell\",\n    \"big-shot\",\n    \"blue-buck\",\n    \"bluebell\",\n    \"bon-voyage\",\n    \"buddy\",\n    \"cool-beans\",\n    \"cosmic\",\n    \"cotton-sky\",\n    \"descent\",\n    \"dewdrop\",\n    \"downdraft\",\n    \"drizzle\",\n    \"dusty-gust\",\n    \"eff-stop\",\n    \"geronimo\",\n    \"grape-soda\",\n    \"helia\",\n    \"high-note\",\n    \"honey-rays\",\n    \"jetstream\",\n    \"laurette\",\n    \"merry-may\",\n    \"mind-freak\",\n    \"muggy-air\",\n    \"parula\",\n    \"pink-cloud\",\n    \"prim-posy\",\n    \"q-t-prism\",\n    \"rain-dance\",\n    \"rainy-day\",\n    \"riverdance\",\n    \"rosewing\",\n    \"sandstorm\",\n    \"serenity\",\n    \"silverwing\",\n    \"sky-flower\",\n    \"skyra\",\n    \"slipstream\",\n    \"snowslide\",\n    \"sugarshine\",\n    \"sunlight\",\n    \"sunny-rays\",\n    \"sunstone\",\n    \"sweet-buzz\",\n    \"tiger-lily\",\n    \"tut-junah\",\n    \"wind-chill\",\n    \"applejack\",\n    \"berryshine\",\n    \"caramel\",\n    \"chip-mint\",\n    \"dr-hooves\",\n    \"felix\",\n    \"hermes\",\n    \"luckette\",\n    \"noteworthy\",\n    \"offbeat\",\n    \"pinkie-pie\",\n    \"pound-cake\",\n    \"rivet\",\n    \"scootaloo\",\n    \"sea-swirl\",\n    \"shoeshine\",\n    \"wisp\",\n    // unicorns\n    \"rarity\",\n    \"bluenote\",\n    \"claude\",\n    \"clear-sky\",\n    \"fire-flare\",\n    \"firelight\",\n    \"flam\",\n    \"flim\",\n    \"hoofdini\",\n    \"jack-pot\",\n    \"jet-set\",\n    \"joe\",\n    \"lily-lace\",\n    \"merry\",\n    \"mistmane\",\n    \"stygian\",\n    \"sunburst\",\n    \"aloha\",\n    \"arpeggio\",\n    \"bags-valet\",\n    \"ballad\",\n    \"beyond\",\n    \"blue-belle\",\n    \"blue-moon\",\n    \"charm\",\n    \"cinnabelle\",\n    \"cold-front\",\n    \"comet-tail\",\n    \"coral-bits\",\n    \"dj-pon-3\",\n    \"dr-steth\",\n    \"eliza\",\n    \"fat-stacks\",\n    \"fine-catch\",\n    \"fine-line\",\n    \"fly-wishes\",\n    \"four-step\",\n    \"foxxy-trot\",\n    \"fresh-coat\",\n    \"giza-hafir\",\n    \"holly-dash\",\n    \"infinity\",\n    \"juno\",\n    \"lemony-gem\",\n    \"lilly-love\",\n    \"log-jam\",\n    \"lolli-love\",\n    \"minuette\",\n    \"mossy-rock\",\n    \"nachtmusik\",\n    \"nixie\",\n    \"nook\",\n    \"orchid-dew\",\n    \"passionate\",\n    \"pinny-lane\",\n    \"pixie\",\n    \"poppycock\",\n    \"precious\",\n    \"rachel-hay\",\n    \"rare-find\",\n    \"red-rose\",\n    \"royal-pin\",\n    \"say-cheese\",\n    \"sea-swirl\",\n    \"slapshot\",\n    \"south-pole\",\n    \"spellbound\",\n    \"sugarberry\",\n    \"sunspot\",\n    \"swan-song\",\n    \"top-marks\",\n    \"undertone\",\n    \"cultivar\",\n    \"daisy\",\n    \"discord\",\n    \"noteworthy\",\n    \"offbeat\",\n    \"quake\",\n    \"red-gala\",\n    \"rose\",\n    \"rosemary\",\n    \"waxton\",\n];\n"
  },
  {
    "path": "lib/rotbart/src/pokemon.rs",
    "content": "pub const POKEDEX: &'static [&'static str] = &[\n    \"abomasnow\",\n    \"abra\",\n    \"absol\",\n    \"accelgor\",\n    \"aegislash\",\n    \"aerodactyl\",\n    \"aggron\",\n    \"aipom\",\n    \"alakazam\",\n    \"alomomola\",\n    \"altaria\",\n    \"amaura\",\n    \"ambipom\",\n    \"amoonguss\",\n    \"ampharos\",\n    \"anorith\",\n    \"araquanid\",\n    \"arbok\",\n    \"arcanine\",\n    \"arceus\",\n    \"archen\",\n    \"archeops\",\n    \"ariados\",\n    \"armaldo\",\n    \"aromatisse\",\n    \"aron\",\n    \"articuno\",\n    \"audino\",\n    \"aurorus\",\n    \"avalugg\",\n    \"axew\",\n    \"azelf\",\n    \"azumarill\",\n    \"azurill\",\n    \"bagon\",\n    \"baltoy\",\n    \"banette\",\n    \"barbaracle\",\n    \"barboach\",\n    \"basculegion\",\n    \"basculin\",\n    \"bastiodon\",\n    \"bayleef\",\n    \"beartic\",\n    \"beautifly\",\n    \"beedrill\",\n    \"beheeyem\",\n    \"beldum\",\n    \"bellossom\",\n    \"bellsprout\",\n    \"bergmite\",\n    \"bewear\",\n    \"bibarel\",\n    \"bidoof\",\n    \"binacle\",\n    \"bisharp\",\n    \"blacephalon\",\n    \"blastoise\",\n    \"blaziken\",\n    \"blissey\",\n    \"blitzle\",\n    \"boldore\",\n    \"bonsly\",\n    \"bouffalant\",\n    \"bounsweet\",\n    \"braixen\",\n    \"braviary\",\n    \"breloom\",\n    \"brionne\",\n    \"bronzong\",\n    \"bronzor\",\n    \"bruxish\",\n    \"budew\",\n    \"buizel\",\n    \"bulbasaur\",\n    \"buneary\",\n    \"bunnelby\",\n    \"burmy\",\n    \"butterfree\",\n    \"buzzwole\",\n    \"cacnea\",\n    \"cacturne\",\n    \"camerupt\",\n    \"carbink\",\n    \"carnivine\",\n    \"carracosta\",\n    \"carvanha\",\n    \"cascoon\",\n    \"castform\",\n    \"caterpie\",\n    \"celebi\",\n    \"celesteela\",\n    \"chandelure\",\n    \"chansey\",\n    \"charizard\",\n    \"charjabug\",\n    \"charmander\",\n    \"charmeleon\",\n    \"chatot\",\n    \"cherrim\",\n    \"cherubi\",\n    \"chesnaught\",\n    \"chespin\",\n    \"chikorita\",\n    \"chimchar\",\n    \"chimecho\",\n    \"chinchou\",\n    \"chingling\",\n    \"cinccino\",\n    \"clamperl\",\n    \"clauncher\",\n    \"clawitzer\",\n    \"claydol\",\n    \"clefable\",\n    \"clefairy\",\n    \"cleffa\",\n    \"cloyster\",\n    \"cobalion\",\n    \"cofagrigus\",\n    \"combee\",\n    \"combusken\",\n    \"comfey\",\n    \"conkeldurr\",\n    \"corphish\",\n    \"corsola\",\n    \"cosmoem\",\n    \"cosmog\",\n    \"cottonee\",\n    \"crabominable\",\n    \"crabrawler\",\n    \"cradily\",\n    \"cranidos\",\n    \"crawdaunt\",\n    \"cresselia\",\n    \"croagunk\",\n    \"crobat\",\n    \"croconaw\",\n    \"crustle\",\n    \"cryogonal\",\n    \"cubchoo\",\n    \"cubone\",\n    \"cutiefly\",\n    \"cyndaquil\",\n    \"darkrai\",\n    \"darmanitan\",\n    \"dartrix\",\n    \"darumaka\",\n    \"decidueye\",\n    \"dedenne\",\n    \"deerling\",\n    \"deino\",\n    \"delcatty\",\n    \"delibird\",\n    \"delphox\",\n    \"deoxys\",\n    \"dewgong\",\n    \"dewott\",\n    \"dewpider\",\n    \"dhelmise\",\n    \"dialga\",\n    \"diancie\",\n    \"diggersby\",\n    \"diglett\",\n    \"ditto\",\n    \"dodrio\",\n    \"doduo\",\n    \"donphan\",\n    \"doublade\",\n    \"dragalge\",\n    \"dragonair\",\n    \"dragonite\",\n    \"drampa\",\n    \"drapion\",\n    \"dratini\",\n    \"drifblim\",\n    \"drifloon\",\n    \"drilbur\",\n    \"drowzee\",\n    \"druddigon\",\n    \"ducklett\",\n    \"dugtrio\",\n    \"dunsparce\",\n    \"duosion\",\n    \"durant\",\n    \"dusclops\",\n    \"dusknoir\",\n    \"duskull\",\n    \"dustox\",\n    \"dwebble\",\n    \"eelektrik\",\n    \"eelektross\",\n    \"eevee\",\n    \"ekans\",\n    \"electabuzz\",\n    \"electivire\",\n    \"electrike\",\n    \"electrode\",\n    \"elekid\",\n    \"elgyem\",\n    \"emboar\",\n    \"emolga\",\n    \"empoleon\",\n    \"enamorus\",\n    \"entei\",\n    \"escavalier\",\n    \"espeon\",\n    \"espurr\",\n    \"excadrill\",\n    \"exeggcute\",\n    \"exeggutor\",\n    \"exploud\",\n    \"farfetch'd\",\n    \"fearow\",\n    \"feebas\",\n    \"fennekin\",\n    \"feraligatr\",\n    \"ferroseed\",\n    \"ferrothorn\",\n    \"finneon\",\n    \"flaaffy\",\n    \"flabébé\",\n    \"flareon\",\n    \"fletchinder\",\n    \"fletchling\",\n    \"floatzel\",\n    \"floette\",\n    \"florges\",\n    \"flygon\",\n    \"fomantis\",\n    \"foongus\",\n    \"forretress\",\n    \"fraxure\",\n    \"frillish\",\n    \"froakie\",\n    \"frogadier\",\n    \"froslass\",\n    \"furfrou\",\n    \"furret\",\n    \"gabite\",\n    \"gallade\",\n    \"galvantula\",\n    \"garbodor\",\n    \"garchomp\",\n    \"gardevoir\",\n    \"gastly\",\n    \"gastrodon\",\n    \"genesect\",\n    \"gengar\",\n    \"geodude\",\n    \"gible\",\n    \"gigalith\",\n    \"girafarig\",\n    \"giratina\",\n    \"glaceon\",\n    \"glalie\",\n    \"glameow\",\n    \"gligar\",\n    \"gliscor\",\n    \"gloom\",\n    \"gogoat\",\n    \"golbat\",\n    \"goldeen\",\n    \"golduck\",\n    \"golem\",\n    \"golett\",\n    \"golisopod\",\n    \"golurk\",\n    \"goodra\",\n    \"goomy\",\n    \"gorebyss\",\n    \"gothita\",\n    \"gothitelle\",\n    \"gothorita\",\n    \"gourgeist\",\n    \"granbull\",\n    \"graveler\",\n    \"greninja\",\n    \"grimer\",\n    \"grotle\",\n    \"groudon\",\n    \"grovyle\",\n    \"growlithe\",\n    \"grubbin\",\n    \"grumpig\",\n    \"gulpin\",\n    \"gumshoos\",\n    \"gurdurr\",\n    \"guzzlord\",\n    \"gyarados\",\n    \"hakamo-o\",\n    \"happiny\",\n    \"hariyama\",\n    \"haunter\",\n    \"hawlucha\",\n    \"haxorus\",\n    \"heatmor\",\n    \"heatran\",\n    \"heliolisk\",\n    \"helioptile\",\n    \"heracross\",\n    \"herdier\",\n    \"hippopotas\",\n    \"hippowdon\",\n    \"hitmonchan\",\n    \"hitmonlee\",\n    \"hitmontop\",\n    \"ho-oh\",\n    \"honchkrow\",\n    \"honedge\",\n    \"hoopa\",\n    \"hoothoot\",\n    \"hoppip\",\n    \"horsea\",\n    \"houndoom\",\n    \"houndour\",\n    \"huntail\",\n    \"hydreigon\",\n    \"hypno\",\n    \"igglybuff\",\n    \"illumise\",\n    \"incineroar\",\n    \"infernape\",\n    \"inkay\",\n    \"ivysaur\",\n    \"jangmo-o\",\n    \"jellicent\",\n    \"jigglypuff\",\n    \"jirachi\",\n    \"jolteon\",\n    \"joltik\",\n    \"jumpluff\",\n    \"jynx\",\n    \"kabuto\",\n    \"kabutops\",\n    \"kadabra\",\n    \"kakuna\",\n    \"kangaskhan\",\n    \"karrablast\",\n    \"kartana\",\n    \"kecleon\",\n    \"keldeo\",\n    \"kingdra\",\n    \"kingler\",\n    \"kirlia\",\n    \"klang\",\n    \"kleavor\",\n    \"klefki\",\n    \"klink\",\n    \"klinklang\",\n    \"koffing\",\n    \"komala\",\n    \"kommo-o\",\n    \"krabby\",\n    \"kricketot\",\n    \"kricketune\",\n    \"krokorok\",\n    \"krookodile\",\n    \"kyogre\",\n    \"kyurem\",\n    \"lairon\",\n    \"lampent\",\n    \"landorus\",\n    \"lanturn\",\n    \"lapras\",\n    \"larvesta\",\n    \"larvitar\",\n    \"latias\",\n    \"latios\",\n    \"leafeon\",\n    \"leavanny\",\n    \"ledian\",\n    \"ledyba\",\n    \"lickilicky\",\n    \"lickitung\",\n    \"liepard\",\n    \"lileep\",\n    \"lilligant\",\n    \"lillipup\",\n    \"linoone\",\n    \"litleo\",\n    \"litten\",\n    \"litwick\",\n    \"lombre\",\n    \"lopunny\",\n    \"lotad\",\n    \"loudred\",\n    \"lucario\",\n    \"ludicolo\",\n    \"lugia\",\n    \"lumineon\",\n    \"lunala\",\n    \"lunatone\",\n    \"lurantis\",\n    \"luvdisc\",\n    \"luxio\",\n    \"luxray\",\n    \"lycanroc\",\n    \"machamp\",\n    \"machoke\",\n    \"machop\",\n    \"magby\",\n    \"magcargo\",\n    \"magearna\",\n    \"magikarp\",\n    \"magmar\",\n    \"magmortar\",\n    \"magnemite\",\n    \"magneton\",\n    \"magnezone\",\n    \"makuhita\",\n    \"malamar\",\n    \"mamoswine\",\n    \"manaphy\",\n    \"mandibuzz\",\n    \"manectric\",\n    \"mankey\",\n    \"mantine\",\n    \"mantyke\",\n    \"maractus\",\n    \"mareanie\",\n    \"mareep\",\n    \"marill\",\n    \"marowak\",\n    \"marshadow\",\n    \"marshtomp\",\n    \"masquerain\",\n    \"mawile\",\n    \"medicham\",\n    \"meditite\",\n    \"meganium\",\n    \"melmetal\",\n    \"meloetta\",\n    \"meltan\",\n    \"meowstic\",\n    \"meowth\",\n    \"mesprit\",\n    \"metagross\",\n    \"metang\",\n    \"metapod\",\n    \"mew\",\n    \"mewtwo\",\n    \"mienfoo\",\n    \"mienshao\",\n    \"mightyena\",\n    \"milotic\",\n    \"miltank\",\n    \"mime-jr\",\n    \"mimikyu\",\n    \"minccino\",\n    \"minior\",\n    \"minun\",\n    \"misdreavus\",\n    \"mismagius\",\n    \"moltres\",\n    \"monferno\",\n    \"morelull\",\n    \"mothim\",\n    \"mr-mime\",\n    \"mudbray\",\n    \"mudkip\",\n    \"mudsdale\",\n    \"muk\",\n    \"munchlax\",\n    \"munna\",\n    \"murkrow\",\n    \"musharna\",\n    \"naganadel\",\n    \"natu\",\n    \"necrozma\",\n    \"nidoking\",\n    \"nidoqueen\",\n    \"nidoran♀\",\n    \"nidoran♂\",\n    \"nidorina\",\n    \"nidorino\",\n    \"nihilego\",\n    \"nincada\",\n    \"ninetales\",\n    \"ninjask\",\n    \"noctowl\",\n    \"noibat\",\n    \"noivern\",\n    \"nosepass\",\n    \"numel\",\n    \"nuzleaf\",\n    \"octillery\",\n    \"oddish\",\n    \"omanyte\",\n    \"omastar\",\n    \"onix\",\n    \"oranguru\",\n    \"oricorio\",\n    \"oshawott\",\n    \"overqwil\",\n    \"pachirisu\",\n    \"palkia\",\n    \"palossand\",\n    \"palpitoad\",\n    \"pancham\",\n    \"pangoro\",\n    \"panpour\",\n    \"pansage\",\n    \"pansear\",\n    \"paras\",\n    \"parasect\",\n    \"passimian\",\n    \"patrat\",\n    \"pawniard\",\n    \"pelipper\",\n    \"persian\",\n    \"petilil\",\n    \"phanpy\",\n    \"phantump\",\n    \"pheromosa\",\n    \"phione\",\n    \"pichu\",\n    \"pidgeot\",\n    \"pidgeotto\",\n    \"pidgey\",\n    \"pidove\",\n    \"pignite\",\n    \"pikachu\",\n    \"pikipek\",\n    \"piloswine\",\n    \"pineco\",\n    \"pinsir\",\n    \"piplup\",\n    \"plusle\",\n    \"poipole\",\n    \"politoed\",\n    \"poliwag\",\n    \"poliwhirl\",\n    \"poliwrath\",\n    \"ponyta\",\n    \"poochyena\",\n    \"popplio\",\n    \"porygon\",\n    \"porygon-z\",\n    \"porygon2\",\n    \"primarina\",\n    \"primeape\",\n    \"prinplup\",\n    \"probopass\",\n    \"psyduck\",\n    \"pumpkaboo\",\n    \"pupitar\",\n    \"purrloin\",\n    \"purugly\",\n    \"pyroar\",\n    \"pyukumuku\",\n    \"quagsire\",\n    \"quilava\",\n    \"quilladin\",\n    \"qwilfish\",\n    \"raichu\",\n    \"raikou\",\n    \"ralts\",\n    \"rampardos\",\n    \"rapidash\",\n    \"raticate\",\n    \"rattata\",\n    \"rayquaza\",\n    \"regice\",\n    \"regigigas\",\n    \"regirock\",\n    \"registeel\",\n    \"relicanth\",\n    \"remoraid\",\n    \"reshiram\",\n    \"reuniclus\",\n    \"rhydon\",\n    \"rhyhorn\",\n    \"rhyperior\",\n    \"ribombee\",\n    \"riolu\",\n    \"rockruff\",\n    \"roggenrola\",\n    \"roselia\",\n    \"roserade\",\n    \"rotom\",\n    \"rowlet\",\n    \"rufflet\",\n    \"sableye\",\n    \"salamence\",\n    \"salandit\",\n    \"salazzle\",\n    \"samurott\",\n    \"sandile\",\n    \"sandshrew\",\n    \"sandslash\",\n    \"sandygast\",\n    \"sawk\",\n    \"sawsbuck\",\n    \"scatterbug\",\n    \"sceptile\",\n    \"scizor\",\n    \"scolipede\",\n    \"scrafty\",\n    \"scraggy\",\n    \"scyther\",\n    \"seadra\",\n    \"seaking\",\n    \"sealeo\",\n    \"seedot\",\n    \"seel\",\n    \"seismitoad\",\n    \"sentret\",\n    \"serperior\",\n    \"servine\",\n    \"seviper\",\n    \"sewaddle\",\n    \"sharpedo\",\n    \"shaymin\",\n    \"shedinja\",\n    \"shelgon\",\n    \"shellder\",\n    \"shellos\",\n    \"shelmet\",\n    \"shieldon\",\n    \"shiftry\",\n    \"shiinotic\",\n    \"shinx\",\n    \"shroomish\",\n    \"shuckle\",\n    \"shuppet\",\n    \"sigilyph\",\n    \"silcoon\",\n    \"silvally\",\n    \"simipour\",\n    \"simisage\",\n    \"simisear\",\n    \"skarmory\",\n    \"skiddo\",\n    \"skiploom\",\n    \"skitty\",\n    \"skorupi\",\n    \"skrelp\",\n    \"skuntank\",\n    \"slaking\",\n    \"slakoth\",\n    \"sliggoo\",\n    \"slowbro\",\n    \"slowking\",\n    \"slowpoke\",\n    \"slugma\",\n    \"slurpuff\",\n    \"smeargle\",\n    \"smoochum\",\n    \"sneasel\",\n    \"sneasler\",\n    \"snivy\",\n    \"snorlax\",\n    \"snorunt\",\n    \"snover\",\n    \"snubbull\",\n    \"solgaleo\",\n    \"solosis\",\n    \"solrock\",\n    \"spearow\",\n    \"spewpa\",\n    \"spheal\",\n    \"spinarak\",\n    \"spinda\",\n    \"spiritomb\",\n    \"spoink\",\n    \"spritzee\",\n    \"squirtle\",\n    \"stakataka\",\n    \"stantler\",\n    \"staraptor\",\n    \"staravia\",\n    \"starly\",\n    \"starmie\",\n    \"staryu\",\n    \"steelix\",\n    \"steenee\",\n    \"stoutland\",\n    \"stufful\",\n    \"stunfisk\",\n    \"stunky\",\n    \"sudowoodo\",\n    \"suicune\",\n    \"sunflora\",\n    \"sunkern\",\n    \"surskit\",\n    \"swablu\",\n    \"swadloon\",\n    \"swalot\",\n    \"swampert\",\n    \"swanna\",\n    \"swellow\",\n    \"swinub\",\n    \"swirlix\",\n    \"swoobat\",\n    \"sylveon\",\n    \"taillow\",\n    \"talonflame\",\n    \"tangela\",\n    \"tangrowth\",\n    \"tapu-bulu\",\n    \"tapu-fini\",\n    \"tapu-koko\",\n    \"tapu-lele\",\n    \"tauros\",\n    \"teddiursa\",\n    \"tentacool\",\n    \"tentacruel\",\n    \"tepig\",\n    \"terrakion\",\n    \"throh\",\n    \"thundurus\",\n    \"timburr\",\n    \"tirtouga\",\n    \"togedemaru\",\n    \"togekiss\",\n    \"togepi\",\n    \"togetic\",\n    \"torchic\",\n    \"torkoal\",\n    \"tornadus\",\n    \"torracat\",\n    \"torterra\",\n    \"totodile\",\n    \"toucannon\",\n    \"toxapex\",\n    \"toxicroak\",\n    \"tranquill\",\n    \"trapinch\",\n    \"treecko\",\n    \"trevenant\",\n    \"tropius\",\n    \"trubbish\",\n    \"trumbeak\",\n    \"tsareena\",\n    \"turtonator\",\n    \"turtwig\",\n    \"tympole\",\n    \"tynamo\",\n    \"type-null\",\n    \"typhlosion\",\n    \"tyranitar\",\n    \"tyrantrum\",\n    \"tyrogue\",\n    \"tyrunt\",\n    \"umbreon\",\n    \"unfezant\",\n    \"unown\",\n    \"ursaluna\",\n    \"ursaring\",\n    \"uxie\",\n    \"vanillish\",\n    \"vanillite\",\n    \"vanilluxe\",\n    \"vaporeon\",\n    \"venipede\",\n    \"venomoth\",\n    \"venonat\",\n    \"venusaur\",\n    \"vespiquen\",\n    \"vibrava\",\n    \"victini\",\n    \"victreebel\",\n    \"vigoroth\",\n    \"vikavolt\",\n    \"vileplume\",\n    \"virizion\",\n    \"vivillon\",\n    \"volbeat\",\n    \"volcanion\",\n    \"volcarona\",\n    \"voltorb\",\n    \"vullaby\",\n    \"vulpix\",\n    \"wailmer\",\n    \"wailord\",\n    \"walrein\",\n    \"wartortle\",\n    \"watchog\",\n    \"weavile\",\n    \"weedle\",\n    \"weepinbell\",\n    \"weezing\",\n    \"whimsicott\",\n    \"whirlipede\",\n    \"whiscash\",\n    \"whismur\",\n    \"wigglytuff\",\n    \"wimpod\",\n    \"wingull\",\n    \"wishiwashi\",\n    \"wobbuffet\",\n    \"woobat\",\n    \"wooper\",\n    \"wormadam\",\n    \"wurmple\",\n    \"wynaut\",\n    \"wyrdeer\",\n    \"xatu\",\n    \"xerneas\",\n    \"xurkitree\",\n    \"yamask\",\n    \"yanma\",\n    \"yanmega\",\n    \"yungoos\",\n    \"yveltal\",\n    \"zangoose\",\n    \"zapdos\",\n    \"zebstrika\",\n    \"zekrom\",\n    \"zeraora\",\n    \"zigzagoon\",\n    \"zoroark\",\n    \"zorua\",\n    \"zubat\",\n    \"zweilous\",\n    \"zygarde\",\n];\n"
  },
  {
    "path": "lib/rotbart/src/xc1.rs",
    "content": "/*!\nhttps://xenoblade.github.io/xb1/bdat/bdat_common/BTL_enelist.html\n\n```javascript\nonlyUnique = (value, index, self) => self.indexOf(value) === index;\nadjectives = [];\nnouns = [];\n\nArray.from(document.getElementsByClassName(\"sortable\")[0].children[1].children)\n    .map(row => row.children[2].innerText)\n    .filter(row => !row.includes(\"(\"))\n    .filter(row => !row.includes(\"'\"))\n    .map(row => row.toLowerCase())\n    .map(row => row.split(\" \"))\n    .filter(row => row.length == 2)\n    .forEach(([adj, noun]) => {\n        adjectives.push(adj);\n        nouns.push(noun);\n    });\n\nadjectives.sort();\nnouns.sort();\nadjectives = adjectives.filter(onlyUnique);\nnouns = nouns.filter(onlyUnique);\n\nconsole.log(JSON.stringify({adjectives, nouns}));\n```\n*/\n\npub const ADJECTIVES: &[&str] = &[\n    \"abnormal\",\n    \"abominable\",\n    \"acid\",\n    \"active\",\n    \"admiral\",\n    \"affluent\",\n    \"aged\",\n    \"ageless\",\n    \"aggressive\",\n    \"agile\",\n    \"agouti\",\n    \"air\",\n    \"amber\",\n    \"ammos\",\n    \"amorous\",\n    \"ancient\",\n    \"android\",\n    \"antibody\",\n    \"antol\",\n    \"aora\",\n    \"apocrypha\",\n    \"aqua\",\n    \"arachno\",\n    \"archer\",\n    \"arel\",\n    \"arena\",\n    \"arm\",\n    \"armoured\",\n    \"arrogant\",\n    \"asara\",\n    \"asha\",\n    \"ashy\",\n    \"assault\",\n    \"atmos\",\n    \"atomis\",\n    \"atomizek\",\n    \"atrophy\",\n    \"aura\",\n    \"avalanche\",\n    \"azul\",\n    \"babel\",\n    \"babeli\",\n    \"baby\",\n    \"baelfael\",\n    \"baelzeb\",\n    \"bagrus\",\n    \"balanced\",\n    \"banquet\",\n    \"barbaric\",\n    \"barbaro\",\n    \"basin\",\n    \"beach\",\n    \"beautiful\",\n    \"benevolent\",\n    \"berserk\",\n    \"big\",\n    \"bizarre\",\n    \"black\",\n    \"blizzard\",\n    \"bois\",\n    \"bono\",\n    \"bonterra\",\n    \"bosque\",\n    \"bow\",\n    \"brabilam\",\n    \"brave\",\n    \"breezy\",\n    \"bright\",\n    \"broken\",\n    \"brutal\",\n    \"buio\",\n    \"bulganon\",\n    \"bunker\",\n    \"buono\",\n    \"butterfly\",\n    \"caelum\",\n    \"calm\",\n    \"canyon\",\n    \"captain\",\n    \"captured\",\n    \"carbon\",\n    \"caris\",\n    \"caura\",\n    \"cautious\",\n    \"cave\",\n    \"cellar\",\n    \"chimai\",\n    \"chloro\",\n    \"chordy\",\n    \"ciconia\",\n    \"clamorous\",\n    \"clandestine\",\n    \"clap\",\n    \"clifftop\",\n    \"clima\",\n    \"clinger\",\n    \"clowd\",\n    \"cold\",\n    \"colony\",\n    \"commander\",\n    \"common\",\n    \"confined\",\n    \"conflagrant\",\n    \"confusion\",\n    \"coppice\",\n    \"corladio\",\n    \"corriente\",\n    \"costa\",\n    \"craft\",\n    \"cratere\",\n    \"crista\",\n    \"cruz\",\n    \"cumulus\",\n    \"cunning\",\n    \"cute\",\n    \"daksha\",\n    \"dark\",\n    \"daughter\",\n    \"dazzling\",\n    \"deadly\",\n    \"decay\",\n    \"defective\",\n    \"defence\",\n    \"defensive\",\n    \"deified\",\n    \"deinos\",\n    \"deluded\",\n    \"demon\",\n    \"desert\",\n    \"despotic\",\n    \"destroyer\",\n    \"destructive\",\n    \"detox\",\n    \"devoted\",\n    \"dim\",\n    \"dinosaur\",\n    \"director\",\n    \"disciple\",\n    \"dogmatic\",\n    \"doom\",\n    \"dorsiar\",\n    \"drakos\",\n    \"drifter\",\n    \"drunk\",\n    \"duel\",\n    \"dummy\",\n    \"easy\",\n    \"eater\",\n    \"egil\",\n    \"ei\",\n    \"elder\",\n    \"elegant\",\n    \"elite\",\n    \"emeraude\",\n    \"enchanting\",\n    \"energy\",\n    \"ent\",\n    \"entma\",\n    \"envy\",\n    \"eques\",\n    \"erratic\",\n    \"eryth\",\n    \"escape\",\n    \"eternal\",\n    \"ether\",\n    \"evil\",\n    \"experienced\",\n    \"experimental\",\n    \"exposure\",\n    \"extra\",\n    \"face\",\n    \"fair\",\n    \"faithful\",\n    \"falsel\",\n    \"fascia\",\n    \"fate\",\n    \"feltl\",\n    \"femuny\",\n    \"ferocious\",\n    \"field\",\n    \"fiendish\",\n    \"fierce\",\n    \"fiery\",\n    \"fighter\",\n    \"final\",\n    \"fine\",\n    \"fio\",\n    \"firework\",\n    \"fiume\",\n    \"flabbergasted\",\n    \"flailing\",\n    \"flamme\",\n    \"flash\",\n    \"flavel\",\n    \"flutes\",\n    \"flying\",\n    \"fool\",\n    \"fork\",\n    \"frenzied\",\n    \"frost\",\n    \"fuchsia\",\n    \"funeral\",\n    \"furious\",\n    \"gadolt\",\n    \"general\",\n    \"gentle\",\n    \"ghostly\",\n    \"giant\",\n    \"gigas\",\n    \"gimran\",\n    \"glacier\",\n    \"gloria\",\n    \"glorious\",\n    \"glory\",\n    \"gluttonous\",\n    \"gluttony\",\n    \"gold\",\n    \"goldi\",\n    \"graceful\",\n    \"gracile\",\n    \"greed\",\n    \"greedy\",\n    \"green\",\n    \"grom\",\n    \"grove\",\n    \"guard\",\n    \"gust\",\n    \"hand\",\n    \"hanz\",\n    \"happiness\",\n    \"hard\",\n    \"hasal\",\n    \"heavy\",\n    \"hidden\",\n    \"hista\",\n    \"hitter\",\n    \"hover\",\n    \"hungry\",\n    \"hyle\",\n    \"illustrious\",\n    \"immovable\",\n    \"impenetrable\",\n    \"indomitable\",\n    \"infernal\",\n    \"inferno\",\n    \"inja\",\n    \"invited\",\n    \"iron\",\n    \"itinerant\",\n    \"itmos\",\n    \"jada\",\n    \"jadals\",\n    \"jade\",\n    \"javelin\",\n    \"jelly\",\n    \"jewel\",\n    \"judicious\",\n    \"jungle\",\n    \"junk\",\n    \"kamikaze\",\n    \"klanis\",\n    \"knuckle\",\n    \"korlba\",\n    \"krawla\",\n    \"krawli\",\n    \"kukukoro\",\n    \"kurian\",\n    \"kyel\",\n    \"lacus\",\n    \"laeklit\",\n    \"lahar\",\n    \"lake\",\n    \"lakebed\",\n    \"lampo\",\n    \"lancer\",\n    \"large\",\n    \"largo\",\n    \"last\",\n    \"latio\",\n    \"lazy\",\n    \"leader\",\n    \"leg\",\n    \"lelepago\",\n    \"leone\",\n    \"licorne\",\n    \"light\",\n    \"lightning\",\n    \"lightspeed\",\n    \"little\",\n    \"living\",\n    \"lograt\",\n    \"lophos\",\n    \"lubum\",\n    \"lunar\",\n    \"lupus\",\n    \"lurker\",\n    \"m35\",\n    \"m56\",\n    \"m59\",\n    \"m85\",\n    \"m87\",\n    \"m88\",\n    \"m97\",\n    \"machine\",\n    \"mad\",\n    \"magestic\",\n    \"magnificent\",\n    \"magnis\",\n    \"maker\",\n    \"makna\",\n    \"maleza\",\n    \"man-eater\",\n    \"marble\",\n    \"marmor\",\n    \"marsh\",\n    \"mass-produced\",\n    \"master\",\n    \"masterful\",\n    \"materia\",\n    \"meat\",\n    \"mechon\",\n    \"meditative\",\n    \"medium\",\n    \"mell\",\n    \"mellow\",\n    \"metal\",\n    \"mild\",\n    \"mining\",\n    \"mischievious\",\n    \"mist\",\n    \"mistol\",\n    \"monta\",\n    \"moonlight\",\n    \"morule\",\n    \"mount\",\n    \"mumkhar\",\n    \"musical\",\n    \"mysterious\",\n    \"mystical\",\n    \"mythical\",\n    \"napping\",\n    \"nero\",\n    \"newgate\",\n    \"niece\",\n    \"night\",\n    \"noble\",\n    \"noto\",\n    \"oasis\",\n    \"obart\",\n    \"obelis\",\n    \"obsessive\",\n    \"offensive\",\n    \"officer\",\n    \"ogre\",\n    \"opulent\",\n    \"ore\",\n    \"orluga\",\n    \"oros\",\n    \"otol\",\n    \"palti\",\n    \"panasowa\",\n    \"pandora\",\n    \"partner\",\n    \"pawn\",\n    \"peeling\",\n    \"pelargos\",\n    \"perna\",\n    \"petra\",\n    \"phoenix\",\n    \"pillager\",\n    \"plain\",\n    \"plane\",\n    \"plasma\",\n    \"plump\",\n    \"poison\",\n    \"poleaxe\",\n    \"polkan\",\n    \"porcu\",\n    \"possessio\",\n    \"powerful\",\n    \"prado\",\n    \"prairie\",\n    \"praying\",\n    \"precious\",\n    \"primo\",\n    \"primordial\",\n    \"prison\",\n    \"prom\",\n    \"proper\",\n    \"prosperous\",\n    \"protective\",\n    \"prudent\",\n    \"puera\",\n    \"pulse\",\n    \"quadrupedal\",\n    \"quarto\",\n    \"queen\",\n    \"quinto\",\n    \"racti\",\n    \"radiant\",\n    \"rage\",\n    \"randa\",\n    \"ranger\",\n    \"ravine\",\n    \"reckless\",\n    \"red\",\n    \"reef\",\n    \"reinforcement\",\n    \"resolute\",\n    \"resplendent\",\n    \"revolutionary\",\n    \"rhoen\",\n    \"ridge\",\n    \"rius\",\n    \"rock\",\n    \"roguish\",\n    \"royal\",\n    \"sabulum\",\n    \"sacred\",\n    \"saldox\",\n    \"sani\",\n    \"sanjibal\",\n    \"satisfied\",\n    \"satorl\",\n    \"scout\",\n    \"sea\",\n    \"secondo\",\n    \"sentimental\",\n    \"sentinel\",\n    \"serene\",\n    \"sero\",\n    \"sesna\",\n    \"sestago\",\n    \"setor\",\n    \"shadeless\",\n    \"shadow\",\n    \"shield\",\n    \"shimmering\",\n    \"sierra\",\n    \"silk\",\n    \"sinful\",\n    \"singing\",\n    \"sky\",\n    \"sloth\",\n    \"slugger\",\n    \"sniper\",\n    \"snowal\",\n    \"snowi\",\n    \"soft\",\n    \"sol\",\n    \"solare\",\n    \"soldier\",\n    \"solid\",\n    \"solidum\",\n    \"somati\",\n    \"sonicia\",\n    \"soothed\",\n    \"sparas\",\n    \"spear\",\n    \"speedy\",\n    \"splendid\",\n    \"stella\",\n    \"stone\",\n    \"storm\",\n    \"stormy\",\n    \"strange\",\n    \"subterranean\",\n    \"suelo\",\n    \"sunlight\",\n    \"sureny\",\n    \"swift\",\n    \"synchronised\",\n    \"tank\",\n    \"tarifa\",\n    \"tele\",\n    \"telethia\",\n    \"tempest\",\n    \"tempestuous\",\n    \"temporal\",\n    \"teneb\",\n    \"tephra\",\n    \"terra\",\n    \"territorial\",\n    \"test\",\n    \"teterra\",\n    \"throne\",\n    \"tocos\",\n    \"tored\",\n    \"trainer\",\n    \"tramont\",\n    \"tranquil\",\n    \"trava\",\n    \"troop\",\n    \"tumultuous\",\n    \"turbulent\",\n    \"turtle\",\n    \"tussock\",\n    \"ucan\",\n    \"ugly\",\n    \"unine\",\n    \"unreliable\",\n    \"upper\",\n    \"uragano\",\n    \"vagabond\",\n    \"vagrant\",\n    \"vague\",\n    \"venaes\",\n    \"venerable\",\n    \"vengeful\",\n    \"verdant\",\n    \"veteran\",\n    \"vicious\",\n    \"victorious\",\n    \"vilae\",\n    \"violent\",\n    \"vivid\",\n    \"wallslide\",\n    \"wandering\",\n    \"wasp\",\n    \"watcher\",\n    \"water\",\n    \"weather\",\n    \"whapol\",\n    \"white\",\n    \"wicked\",\n    \"willow\",\n    \"wind\",\n    \"wise\",\n    \"wizard\",\n    \"woeful\",\n    \"wood\",\n    \"wool\",\n    \"worker\",\n    \"wrathful\",\n    \"xord\",\n    \"yellow\",\n    \"young\",\n    \"zanza\",\n    \"zealous\",\n    \"zefa\",\n    \"zegia\",\n    \"zeldi\",\n];\n\npub const NOUNS: &[&str] = &[\n    \"1\",\n    \"2\",\n    \"a\",\n    \"abaasy\",\n    \"acon\",\n    \"ageshu\",\n    \"aglovale\",\n    \"aim\",\n    \"albatro\",\n    \"alfead\",\n    \"allocer\",\n    \"altrich\",\n    \"amon\",\n    \"andante\",\n    \"andos\",\n    \"ansel\",\n    \"anstan\",\n    \"antol\",\n    \"anzabi\",\n    \"apety\",\n    \"apis\",\n    \"arachno\",\n    \"arca\",\n    \"ardun\",\n    \"arielle\",\n    \"aries\",\n    \"armu\",\n    \"arsene\",\n    \"astas\",\n    \"atrophy\",\n    \"auburn\",\n    \"b\",\n    \"balgas\",\n    \"balteid\",\n    \"bana\",\n    \"bandaz\",\n    \"barbas\",\n    \"barbatos\",\n    \"barg\",\n    \"barnaby\",\n    \"bathin\",\n    \"bayern\",\n    \"behemoth\",\n    \"belagon\",\n    \"beleth\",\n    \"belgazas\",\n    \"belmo\",\n    \"bifrons\",\n    \"bird\",\n    \"bluchal\",\n    \"bluco\",\n    \"bors\",\n    \"botis\",\n    \"bracken\",\n    \"brog\",\n    \"buer\",\n    \"bug\",\n    \"bugworm\",\n    \"bune\",\n    \"bunnia\",\n    \"bunnit\",\n    \"bunnitzol\",\n    \"bunniv\",\n    \"butterfly\",\n    \"c\",\n    \"captain\",\n    \"cardamon\",\n    \"caterpile\",\n    \"chilkin\",\n    \"commander\",\n    \"cornelius\",\n    \"crawler\",\n    \"crocell\",\n    \"dablon\",\n    \"daedala\",\n    \"danaemos\",\n    \"daulton\",\n    \"deinos\",\n    \"device\",\n    \"dickson\",\n    \"digalus\",\n    \"donnis\",\n    \"dorothea\",\n    \"dragon\",\n    \"dummy\",\n    \"e\",\n    \"edegia\",\n    \"eduardo\",\n    \"egel\",\n    \"egil\",\n    \"ei\",\n    \"ekidno\",\n    \"eks\",\n    \"eligos\",\n    \"eluca\",\n    \"empress\",\n    \"entia\",\n    \"eugen\",\n    \"exception\",\n    \"face\",\n    \"felix\",\n    \"feris\",\n    \"fischer\",\n    \"fish\",\n    \"fla\",\n    \"flamii\",\n    \"flamral\",\n    \"flier\",\n    \"florence\",\n    \"focalor\",\n    \"forte\",\n    \"frengel\",\n    \"frog\",\n    \"ga\",\n    \"gaheris\",\n    \"galdo\",\n    \"galdon\",\n    \"galgaron\",\n    \"galvin\",\n    \"gamigin\",\n    \"gawain\",\n    \"geldesia\",\n    \"generator\",\n    \"gigapur\",\n    \"godwin\",\n    \"gogol\",\n    \"goliante\",\n    \"golteus\",\n    \"gonzalez\",\n    \"gozra\",\n    \"grady\",\n    \"gragus\",\n    \"gravar\",\n    \"gremory\",\n    \"gross\",\n    \"grune\",\n    \"guardian\",\n    \"gwynry\",\n    \"harmelon\",\n    \"heinrich\",\n    \"hiln\",\n    \"hode\",\n    \"holand\",\n    \"hox\",\n    \"igna\",\n    \"imlaly\",\n    \"impulso\",\n    \"ipos\",\n    \"jerome\",\n    \"jozan\",\n    \"juju\",\n    \"jurom\",\n    \"jutard\",\n    \"kaelin\",\n    \"king\",\n    \"kircheis\",\n    \"kisling\",\n    \"klesida\",\n    \"konev\",\n    \"krabble\",\n    \"kromar\",\n    \"labolas\",\n    \"laia\",\n    \"lamorak\",\n    \"lancelot\",\n    \"lecrough\",\n    \"leraje\",\n    \"lesunia\",\n    \"lexos\",\n    \"lizard\",\n    \"long-distance\",\n    \"lorithia\",\n    \"m104\",\n    \"m31\",\n    \"m32\",\n    \"m32x\",\n    \"m42\",\n    \"m46x\",\n    \"m51\",\n    \"m53\",\n    \"m53x\",\n    \"m55\",\n    \"m63\",\n    \"m64\",\n    \"m64x\",\n    \"m67\",\n    \"m69\",\n    \"m69x\",\n    \"m71\",\n    \"m72\",\n    \"m82\",\n    \"m84\",\n    \"m86\",\n    \"machine\",\n    \"magdalena\",\n    \"mahatos\",\n    \"mammut\",\n    \"marcus\",\n    \"marin\",\n    \"matrix\",\n    \"mechon\",\n    \"medorlo\",\n    \"moabit\",\n    \"moramora\",\n    \"morax\",\n    \"mordred\",\n    \"mu\",\n    \"mumkhar\",\n    \"murakmor\",\n    \"naberius\",\n    \"nebula\",\n    \"nemesis\",\n    \"nova\",\n    \"obart\",\n    \"oracion\",\n    \"ories\",\n    \"orluga\",\n    \"orobas\",\n    \"orthlus\",\n    \"pagul\",\n    \"paimon\",\n    \"palamedes\",\n    \"palsadia\",\n    \"paramecia\",\n    \"patrichev\",\n    \"pavlovsk\",\n    \"piranhax\",\n    \"pod\",\n    \"ponio\",\n    \"prototype\",\n    \"pterix\",\n    \"purson\",\n    \"quadwing\",\n    \"queen\",\n    \"ragoel\",\n    \"ramshyde\",\n    \"raxeal\",\n    \"redrob\",\n    \"retrato\",\n    \"rezno\",\n    \"rhana\",\n    \"rhangrot\",\n    \"rhogul\",\n    \"rhogulia\",\n    \"robusto\",\n    \"rockwell\",\n    \"rodriguez\",\n    \"ronove\",\n    \"rotbart\",\n    \"rufus\",\n    \"sallos\",\n    \"salvacion\",\n    \"sardi\",\n    \"sauros\",\n    \"schvaik\",\n    \"scout\",\n    \"selua\",\n    \"sergeant\",\n    \"shellfish\",\n    \"sitri\",\n    \"skeeter\",\n    \"skyray\",\n    \"slobos\",\n    \"sonid\",\n    \"sprahda\",\n    \"subspecies\",\n    \"taos\",\n    \"te\",\n    \"tele\",\n    \"telethia\",\n    \"tentacle\",\n    \"tirkin\",\n    \"tokilos\",\n    \"tolosnia\",\n    \"torquidon\",\n    \"torta\",\n    \"tristan\",\n    \"tuber\",\n    \"tude\",\n    \"turtle\",\n    \"type\",\n    \"upa\",\n    \"vagul\",\n    \"valencia\",\n    \"vanflare\",\n    \"vang\",\n    \"varla\",\n    \"vassago\",\n    \"volfen\",\n    \"volff\",\n    \"watchtower\",\n    \"widardun\",\n    \"wisp\",\n    \"wolfol\",\n    \"xord\",\n    \"yado\",\n    \"yozel\",\n    \"zagamei\",\n    \"zanden\",\n    \"zanza\",\n    \"zektol\",\n    \"zepar\",\n    \"zo\",\n    \"zolos\",\n    \"zomar\",\n];\n"
  },
  {
    "path": "lib/rotbart/src/xc2.rs",
    "content": "/*!\nhttps://xenoblade.github.io/xb2/bdat/common/BTL_EnBook.html\n\n\n```javascript\nonlyUnique = (value, index, self) => self.indexOf(value) === index;\nadjectives = [];\nnouns = [];\n\nArray.from(document.getElementsByClassName(\"sortable\")[0].children[1].children)\n    .map(row => row.children[2].innerText)\n    .filter(row => !row.includes(\"(\"))\n    .filter(row => !row.includes(\"'\"))\n    .map(row => row.toLowerCase())\n    .map(row => row.split(\" \"))\n    .filter(row => row.length == 2)\n    .forEach(([adj, noun]) => {\n        adjectives.push(adj);\n        nouns.push(noun);\n    });\n\nadjectives.sort();\nnouns.sort();\nadjectives = adjectives.filter(onlyUnique);\nnouns = nouns.filter(onlyUnique);\n\nconsole.log(JSON.stringify({adjectives, nouns}));\n```\n*/\n\npub const ADJECTIVES: &[&str] = &[\n    \"aatoban\",\n    \"abrachi\",\n    \"acar\",\n    \"acenia\",\n    \"acute\",\n    \"agam\",\n    \"ahaato\",\n    \"ahaid\",\n    \"alda\",\n    \"alonzo\",\n    \"amari\",\n    \"amman\",\n    \"amost\",\n    \"anbu\",\n    \"ancient\",\n    \"anguished\",\n    \"antecedent\",\n    \"antipathetic\",\n    \"aplom\",\n    \"arcah\",\n    \"archer\",\n    \"ardainian\",\n    \"arlo\",\n    \"armor\",\n    \"armored\",\n    \"arno\",\n    \"arogan\",\n    \"arrah\",\n    \"arrow\",\n    \"artifice\",\n    \"asset\",\n    \"astle\",\n    \"astor\",\n    \"atrocious\",\n    \"aurea\",\n    \"autumn-shower\",\n    \"avero\",\n    \"avys\",\n    \"awarth\",\n    \"aybam\",\n    \"azure\",\n    \"bafoo\",\n    \"bagis\",\n    \"bagoan\",\n    \"baigun\",\n    \"barz\",\n    \"bauz\",\n    \"beast-hunter\",\n    \"beat\",\n    \"beatific\",\n    \"bebelk\",\n    \"bebth\",\n    \"belgio\",\n    \"berserker\",\n    \"biban\",\n    \"biblis\",\n    \"birial\",\n    \"blade\",\n    \"bland\",\n    \"bledku\",\n    \"blood\",\n    \"blue\",\n    \"blue-eyed\",\n    \"bobbile\",\n    \"bohn\",\n    \"bolc\",\n    \"boss\",\n    \"brave\",\n    \"brazay\",\n    \"breed\",\n    \"brewl\",\n    \"bright\",\n    \"brionac\",\n    \"brish\",\n    \"brogen\",\n    \"broog\",\n    \"brutton\",\n    \"bubble\",\n    \"buden\",\n    \"buma\",\n    \"burran\",\n    \"burrig\",\n    \"caliber\",\n    \"canzin\",\n    \"captain\",\n    \"carbis\",\n    \"cardine\",\n    \"cardorl\",\n    \"cartbreaker\",\n    \"cascade\",\n    \"cave\",\n    \"celsars\",\n    \"chefko\",\n    \"chelta\",\n    \"chibal\",\n    \"chickenheart\",\n    \"childre\",\n    \"chituk\",\n    \"clabor\",\n    \"clap\",\n    \"climactic\",\n    \"climber\",\n    \"cling\",\n    \"cloche\",\n    \"cobalt\",\n    \"colnicas\",\n    \"confiscator\",\n    \"coora\",\n    \"crane\",\n    \"crawler\",\n    \"crimson\",\n    \"cunning\",\n    \"cursed\",\n    \"dagus\",\n    \"dajan\",\n    \"dakhim\",\n    \"dalakio\",\n    \"dalian\",\n    \"dalya\",\n    \"damodan\",\n    \"damp\",\n    \"darkblood\",\n    \"darml\",\n    \"dayvol\",\n    \"deadfire\",\n    \"decapitator\",\n    \"dedicated\",\n    \"deej\",\n    \"deep-green\",\n    \"deidon\",\n    \"deimos\",\n    \"demon\",\n    \"demon-shell\",\n    \"derrah\",\n    \"desert\",\n    \"dettl\",\n    \"diggel\",\n    \"dirid\",\n    \"disas\",\n    \"dobri\",\n    \"docel\",\n    \"dockle\",\n    \"doltom\",\n    \"dominal\",\n    \"dormic\",\n    \"dormine\",\n    \"dorrl\",\n    \"doryu\",\n    \"drac\",\n    \"dread\",\n    \"dreadnik\",\n    \"drive\",\n    \"droth\",\n    \"drub\",\n    \"drum\",\n    \"drux\",\n    \"duel\",\n    \"dusky\",\n    \"dux\",\n    \"dynal\",\n    \"eanl\",\n    \"eclipse\",\n    \"effi\",\n    \"elder\",\n    \"elidor\",\n    \"emerald\",\n    \"empress\",\n    \"emton\",\n    \"enada\",\n    \"engineer\",\n    \"enlightened\",\n    \"epicurean\",\n    \"episto\",\n    \"erratic\",\n    \"ers\",\n    \"eryagh\",\n    \"esko\",\n    \"espina\",\n    \"everdark\",\n    \"evileye\",\n    \"excavator\",\n    \"fabel\",\n    \"fact\",\n    \"falc\",\n    \"familion\",\n    \"fane\",\n    \"faros\",\n    \"fayl\",\n    \"felusi\",\n    \"femni\",\n    \"fend\",\n    \"ferrii\",\n    \"fers\",\n    \"fiar\",\n    \"field\",\n    \"figgle\",\n    \"firm\",\n    \"fissa\",\n    \"flanck\",\n    \"flash\",\n    \"fleet\",\n    \"flink\",\n    \"float\",\n    \"flow\",\n    \"fort\",\n    \"fradde\",\n    \"fratte\",\n    \"fresh\",\n    \"froga\",\n    \"fubbl\",\n    \"funcel\",\n    \"fuvor\",\n    \"gabnun\",\n    \"gabondo\",\n    \"gaddon\",\n    \"galahem\",\n    \"gamen\",\n    \"gaoid\",\n    \"gareid\",\n    \"gargen\",\n    \"garnia\",\n    \"garon\",\n    \"gast\",\n    \"gattle\",\n    \"gazust\",\n    \"gazzam\",\n    \"gefillon\",\n    \"gemini\",\n    \"genni\",\n    \"gerolf\",\n    \"ghudan\",\n    \"giga\",\n    \"giron\",\n    \"gladiator\",\n    \"glamorous\",\n    \"glaw\",\n    \"glorious\",\n    \"glorr\",\n    \"glox\",\n    \"gneo\",\n    \"gobeen\",\n    \"goldol\",\n    \"goliath\",\n    \"gorian\",\n    \"gourmand\",\n    \"graaz\",\n    \"grad\",\n    \"grads\",\n    \"grandum\",\n    \"grash\",\n    \"grass\",\n    \"grat\",\n    \"gravur\",\n    \"gray\",\n    \"graze\",\n    \"greetz\",\n    \"grievous\",\n    \"grohl\",\n    \"gronta\",\n    \"ground\",\n    \"growsa\",\n    \"gulnid\",\n    \"gyan\",\n    \"haaken\",\n    \"haldood\",\n    \"handwringing\",\n    \"harbinger\",\n    \"hard\",\n    \"hard-bitten\",\n    \"hardl\",\n    \"haywire\",\n    \"hazan\",\n    \"hazzard\",\n    \"heggl\",\n    \"heidl\",\n    \"heroic\",\n    \"highbohn\",\n    \"highscreeb\",\n    \"hool\",\n    \"hooligan\",\n    \"horiz\",\n    \"howitzer\",\n    \"hungry\",\n    \"huust\",\n    \"igard\",\n    \"illumi\",\n    \"imba\",\n    \"immovable\",\n    \"impassable\",\n    \"implacable\",\n    \"incandescent\",\n    \"indoline\",\n    \"infernal\",\n    \"ingle\",\n    \"insectivore\",\n    \"insufferable\",\n    \"interceptor\",\n    \"ionospheric\",\n    \"jadas\",\n    \"jadde\",\n    \"jadeite\",\n    \"jailer\",\n    \"jaim\",\n    \"jakki\",\n    \"javelin\",\n    \"jenth\",\n    \"jewel\",\n    \"joobs\",\n    \"jubel\",\n    \"judicial\",\n    \"jumbri\",\n    \"kadar\",\n    \"kalymon\",\n    \"kanoo\",\n    \"karlin\",\n    \"karor\",\n    \"karyl\",\n    \"kast\",\n    \"kattl\",\n    \"keat\",\n    \"kendra\",\n    \"king\",\n    \"kitarmo\",\n    \"klaret\",\n    \"klim\",\n    \"knoober\",\n    \"koror\",\n    \"kran\",\n    \"krim\",\n    \"kustal\",\n    \"lafda\",\n    \"lance\",\n    \"land\",\n    \"lapis\",\n    \"lapse\",\n    \"latollo\",\n    \"leaf\",\n    \"leap\",\n    \"ledro\",\n    \"lefth\",\n    \"legarre\",\n    \"leggin\",\n    \"legia\",\n    \"lekut\",\n    \"leo\",\n    \"leonine\",\n    \"leran\",\n    \"lethal\",\n    \"levma\",\n    \"liar\",\n    \"libelte\",\n    \"liberion\",\n    \"ligar\",\n    \"limdo\",\n    \"lindwurm\",\n    \"linka\",\n    \"little\",\n    \"lun\",\n    \"lunar\",\n    \"mabalus\",\n    \"mabas\",\n    \"mabluk\",\n    \"machine-gun\",\n    \"madoline\",\n    \"magmund\",\n    \"magnl\",\n    \"magra\",\n    \"mahi\",\n    \"mahn\",\n    \"mailer\",\n    \"mailoo\",\n    \"majacan\",\n    \"makfur\",\n    \"malicious\",\n    \"man-eating\",\n    \"manda\",\n    \"mant\",\n    \"maramal\",\n    \"marauder\",\n    \"margl\",\n    \"margot\",\n    \"marna\",\n    \"martial\",\n    \"martz\",\n    \"masque\",\n    \"massido\",\n    \"mayn\",\n    \"mees\",\n    \"megabroot\",\n    \"megalo\",\n    \"meldl\",\n    \"melm\",\n    \"melz\",\n    \"menacing\",\n    \"mergen\",\n    \"meson\",\n    \"messar\",\n    \"messenger\",\n    \"mia\",\n    \"misdan\",\n    \"mishgal\",\n    \"mogen\",\n    \"moist\",\n    \"moonlighting\",\n    \"mordow\",\n    \"morg\",\n    \"moskel\",\n    \"mukkle\",\n    \"muscley\",\n    \"myrmidon\",\n    \"myrrhes\",\n    \"nairoo\",\n    \"nant\",\n    \"natto\",\n    \"nebri\",\n    \"nefto\",\n    \"nekrino\",\n    \"nel\",\n    \"neleid\",\n    \"nelva\",\n    \"neml\",\n    \"nemus\",\n    \"nereus\",\n    \"nilhez\",\n    \"nitpicking\",\n    \"noble\",\n    \"noggle\",\n    \"noigan\",\n    \"noign\",\n    \"nomad\",\n    \"nomadic\",\n    \"nomul\",\n    \"noog\",\n    \"nookka\",\n    \"norgam\",\n    \"nose\",\n    \"nossi\",\n    \"novl\",\n    \"nowaak\",\n    \"nula\",\n    \"nuruba\",\n    \"nutch\",\n    \"nyol\",\n    \"obri\",\n    \"obsi\",\n    \"odolera\",\n    \"olphen\",\n    \"oone\",\n    \"organl\",\n    \"overaffectionate\",\n    \"pactusk\",\n    \"pain\",\n    \"pallov\",\n    \"parady\",\n    \"parasite\",\n    \"parole\",\n    \"pawn\",\n    \"peerless\",\n    \"peri\",\n    \"pernicious\",\n    \"perplexed\",\n    \"phantom\",\n    \"phoebus\",\n    \"pidor\",\n    \"pinch\",\n    \"pipe\",\n    \"piros\",\n    \"poison\",\n    \"praetorian\",\n    \"presser\",\n    \"pride\",\n    \"prink\",\n    \"prom\",\n    \"psit\",\n    \"pugli\",\n    \"quake\",\n    \"queen\",\n    \"rabres\",\n    \"radclyffe\",\n    \"radliev\",\n    \"radyo\",\n    \"raflas\",\n    \"raging\",\n    \"raider\",\n    \"ralsh\",\n    \"rambl\",\n    \"rangel\",\n    \"ranger\",\n    \"rankor\",\n    \"ransro\",\n    \"rapturous\",\n    \"ratchet\",\n    \"ravenwing\",\n    \"ravine\",\n    \"razor\",\n    \"reast\",\n    \"rebra\",\n    \"rebul\",\n    \"rebus\",\n    \"red\",\n    \"redom\",\n    \"reed\",\n    \"reeg\",\n    \"reeking\",\n    \"reener\",\n    \"regel\",\n    \"reggl\",\n    \"regodos\",\n    \"regus\",\n    \"rekon\",\n    \"remorseful\",\n    \"revl\",\n    \"reyo\",\n    \"ribage\",\n    \"rinker\",\n    \"rip\",\n    \"ripbik\",\n    \"rippl\",\n    \"rivarl\",\n    \"river\",\n    \"riveral\",\n    \"robal\",\n    \"robol\",\n    \"rock\",\n    \"rondel\",\n    \"rook\",\n    \"rooka\",\n    \"roose\",\n    \"rowda\",\n    \"ruchik\",\n    \"rudoni\",\n    \"ruffian\",\n    \"ruga\",\n    \"saber\",\n    \"sable\",\n    \"sabri\",\n    \"sad\",\n    \"salsh\",\n    \"salteau\",\n    \"sammel\",\n    \"samoo\",\n    \"sandi\",\n    \"sandl\",\n    \"sarabashi\",\n    \"sarchess\",\n    \"scowling\",\n    \"scribo\",\n    \"scura\",\n    \"scurvy\",\n    \"security\",\n    \"segel\",\n    \"selmo\",\n    \"sequestered\",\n    \"serpentine\",\n    \"seveeto\",\n    \"shadow\",\n    \"sharion\",\n    \"shezl\",\n    \"shield\",\n    \"shimmun\",\n    \"shralk\",\n    \"shreddle\",\n    \"shungle\",\n    \"sinon\",\n    \"skad\",\n    \"skeeter\",\n    \"skode\",\n    \"skyfist\",\n    \"slade\",\n    \"slayg\",\n    \"sleepwalker\",\n    \"slithe\",\n    \"sloam\",\n    \"sloth\",\n    \"small\",\n    \"smart\",\n    \"snide\",\n    \"sniping\",\n    \"snowdol\",\n    \"somelia\",\n    \"soothsayer\",\n    \"sorbl\",\n    \"sordis\",\n    \"sorolle\",\n    \"soul-eater\",\n    \"sowl\",\n    \"spanner\",\n    \"sparda\",\n    \"speed\",\n    \"spellbinder\",\n    \"spike\",\n    \"spinel\",\n    \"spirit\",\n    \"spitt\",\n    \"sprack\",\n    \"spring\",\n    \"spring-shower\",\n    \"stark\",\n    \"steeky\",\n    \"stellar\",\n    \"sting\",\n    \"stonic\",\n    \"straat\",\n    \"strom\",\n    \"sugar\",\n    \"supercharged\",\n    \"supporter\",\n    \"survee\",\n    \"svena\",\n    \"sweeper\",\n    \"sweet\",\n    \"sygian\",\n    \"talent\",\n    \"tales\",\n    \"tannia\",\n    \"tantalese\",\n    \"tattooed\",\n    \"tawa\",\n    \"telah\",\n    \"telen\",\n    \"telgoo\",\n    \"tempest\",\n    \"teppus\",\n    \"territorial\",\n    \"tetora\",\n    \"tiquil\",\n    \"tirkin\",\n    \"tolen\",\n    \"tolmeda\",\n    \"tomlok\",\n    \"tonbre\",\n    \"torrl\",\n    \"totorio\",\n    \"trainer\",\n    \"trets\",\n    \"trilut\",\n    \"trock\",\n    \"troog\",\n    \"tsorrid\",\n    \"twondus\",\n    \"typhon\",\n    \"tyrannotitan\",\n    \"uis\",\n    \"uluran\",\n    \"uncid\",\n    \"unflinching\",\n    \"urbs\",\n    \"urobas\",\n    \"vabra\",\n    \"vagrant\",\n    \"vaids\",\n    \"valkan\",\n    \"vallum\",\n    \"valt\",\n    \"valta\",\n    \"vashar\",\n    \"vaugel\",\n    \"vay\",\n    \"velvan\",\n    \"venal\",\n    \"ventts\",\n    \"victor\",\n    \"vile\",\n    \"vint\",\n    \"violent\",\n    \"vogar\",\n    \"vokkon\",\n    \"vool\",\n    \"wacon\",\n    \"wall\",\n    \"watcher\",\n    \"water\",\n    \"wendel\",\n    \"werval\",\n    \"whisp\",\n    \"whispering\",\n    \"winter\",\n    \"wood\",\n    \"wormeater\",\n    \"wrath\",\n    \"xane\",\n    \"yardl\",\n    \"yellow\",\n    \"young\",\n    \"youse\",\n    \"yurem\",\n    \"zafirah\",\n    \"zaguin\",\n    \"zalidor\",\n    \"zamban\",\n    \"zangiv\",\n    \"zardl\",\n    \"zekor\",\n    \"zeld\",\n    \"zeoth\",\n    \"zext\",\n    \"ziggan\",\n    \"zigul\",\n    \"zike\",\n    \"zoke\",\n    \"zooz\",\n];\n\npub const NOUNS: &[&str] = &[\n    \"ageshu\",\n    \"aion\",\n    \"alfonso\",\n    \"alfred\",\n    \"aligo\",\n    \"amaruq\",\n    \"anlood\",\n    \"ansel\",\n    \"antol\",\n    \"aplacus\",\n    \"arachno\",\n    \"archibald\",\n    \"ardun\",\n    \"argus\",\n    \"aries\",\n    \"armu\",\n    \"aspar\",\n    \"aspid\",\n    \"baldr\",\n    \"balgas\",\n    \"beaufort\",\n    \"behemoth\",\n    \"benf\",\n    \"bernard\",\n    \"beru\",\n    \"bigelow\",\n    \"billy\",\n    \"blant\",\n    \"bot\",\n    \"brennan\",\n    \"brent\",\n    \"brog\",\n    \"bufa\",\n    \"buloofo\",\n    \"bunnit\",\n    \"camill\",\n    \"caterpile\",\n    \"cavill\",\n    \"cetus\",\n    \"citadel\",\n    \"clive\",\n    \"colossus\",\n    \"conroy\",\n    \"crustip\",\n    \"curtis\",\n    \"dagmara\",\n    \"damian\",\n    \"darius\",\n    \"derrick\",\n    \"dimitri\",\n    \"douglas\",\n    \"driver\",\n    \"dylan\",\n    \"edgar\",\n    \"edwin\",\n    \"egg\",\n    \"ekidno\",\n    \"eks\",\n    \"elliott\",\n    \"ellook\",\n    \"eluca\",\n    \"elwyn\",\n    \"emblem\",\n    \"erg\",\n    \"eugene\",\n    \"feris\",\n    \"fighter\",\n    \"flamii\",\n    \"flier\",\n    \"galgan\",\n    \"garaffa\",\n    \"garlus\",\n    \"gerald\",\n    \"glenn\",\n    \"gogol\",\n    \"goliante\",\n    \"gonzalez\",\n    \"grace\",\n    \"grady\",\n    \"grebel\",\n    \"griffox\",\n    \"grzeg\",\n    \"guldo\",\n    \"gyanna\",\n    \"hermes\",\n    \"hiln\",\n    \"honnold\",\n    \"howard\",\n    \"hox\",\n    \"hugo\",\n    \"igna\",\n    \"jacob\",\n    \"jagron\",\n    \"jimmy\",\n    \"jo\",\n    \"julio\",\n    \"kamron\",\n    \"kapiba\",\n    \"keeper\",\n    \"knight\",\n    \"kollin\",\n    \"korbin\",\n    \"krabble\",\n    \"kurodil\",\n    \"kustal\",\n    \"laia\",\n    \"leon\",\n    \"lexos\",\n    \"ligia\",\n    \"lizard\",\n    \"locks\",\n    \"loyalist\",\n    \"ludd\",\n    \"lysaat\",\n    \"madadh\",\n    \"major\",\n    \"malcom\",\n    \"mambor\",\n    \"mammut\",\n    \"marcus\",\n    \"marrin\",\n    \"marvin\",\n    \"medea\",\n    \"medooz\",\n    \"melvin\",\n    \"melvyn\",\n    \"milltear\",\n    \"mitchell\",\n    \"montgomery\",\n    \"moramora\",\n    \"mork\",\n    \"morris\",\n    \"murph\",\n    \"muller\",\n    \"nest\",\n    \"nipper\",\n    \"ophelia\",\n    \"ophion\",\n    \"ories\",\n    \"orion\",\n    \"oscar\",\n    \"padraig\",\n    \"pagul\",\n    \"parisax\",\n    \"peng\",\n    \"phoebus\",\n    \"pippito\",\n    \"piranhax\",\n    \"plambus\",\n    \"pod\",\n    \"polly\",\n    \"ponio\",\n    \"private\",\n    \"pterix\",\n    \"puffot\",\n    \"quadwing\",\n    \"quincy\",\n    \"radclyffe\",\n    \"rapchor\",\n    \"reginald\",\n    \"remington\",\n    \"rhana\",\n    \"rhinon\",\n    \"rhogul\",\n    \"rider\",\n    \"riik\",\n    \"rodonya\",\n    \"ropl\",\n    \"rosa\",\n    \"rotbart\",\n    \"rott\",\n    \"runner\",\n    \"rusholme\",\n    \"sadie\",\n    \"saggie\",\n    \"sauros\",\n    \"saxton\",\n    \"scandia\",\n    \"scorpox\",\n    \"scout\",\n    \"sentinel\",\n    \"sentry\",\n    \"sergeant\",\n    \"serprond\",\n    \"seàirdeant\",\n    \"siren\",\n    \"skeet\",\n    \"skeeter\",\n    \"skull\",\n    \"skwaror\",\n    \"snaidhpear\",\n    \"soldier\",\n    \"sollmeyer\",\n    \"solomon\",\n    \"sovereign\",\n    \"squood\",\n    \"standard\",\n    \"stanley\",\n    \"star\",\n    \"stein\",\n    \"stoyan\",\n    \"symbol\",\n    \"tanca\",\n    \"taos\",\n    \"tirkin\",\n    \"totem\",\n    \"trupair\",\n    \"ulysses\",\n    \"upa\",\n    \"urchon\",\n    \"vaclav\",\n    \"vang\",\n    \"volff\",\n    \"william\",\n    \"wisp\",\n    \"xavier\",\n    \"xiaxia\",\n];\n\npub const COMMON_BLADES: &'static [&'static str] = &[\n    \"aizen\",\n    \"akatsuki\",\n    \"akebono\",\n    \"azai\",\n    \"arai\",\n    \"arufumi\",\n    \"ikazuchi\",\n    \"ikaruga\",\n    \"izayoi\",\n    \"izumo\",\n    \"ichiro\",\n    \"ikaku\",\n    \"ikki\",\n    \"issen\",\n    \"inazuma\",\n    \"ushio\",\n    \"oryuu\",\n    \"owashi\",\n    \"okina\",\n    \"oboro\",\n    \"orochi\",\n    \"kaiden\",\n    \"kaibyaku\",\n    \"karkan\",\n    \"kagemitsu\",\n    \"kagero\",\n    \"kazan\",\n    \"katsumasa\",\n    \"kanesada\",\n    \"kanehira\",\n    \"kanemitsu\",\n    \"kei\",\n    \"kijin\",\n    \"kibitsu\",\n    \"gokuto\",\n    \"kirim\",\n    \"gingar\",\n    \"kur\",\n    \"kuzan\",\n    \"kusanagi\",\n    \"kurochi\",\n    \"krogane\",\n    \"genno\",\n    \"kouki\",\n    \"kouru\",\n    \"kogarashi\",\n    \"kokras\",\n    \"gogyo\",\n    \"kojiro\",\n    \"kosor\",\n    \"kotetsu\",\n    \"kongir\",\n    \"konjiki\",\n    \"sakon\",\n    \"sangar\",\n    \"shun\",\n    \"shikiso\",\n    \"shishi\",\n    \"shichisei\",\n    \"shiko\",\n    \"shippun\",\n    \"shiden\",\n    \"shura\",\n    \"shungen\",\n    \"shin-mei\",\n    \"shinra\",\n    \"jin-rai\",\n    \"jinryuu\",\n    \"suro\",\n    \"sulgar\",\n    \"seigai\",\n    \"sysor\",\n    \"seiten\",\n    \"seimei\",\n    \"zeku\",\n    \"zeno\",\n    \"sordai\",\n    \"soten\",\n    \"sohmei\",\n    \"shouryu\",\n    \"sohaya\",\n    \"taiga\",\n    \"daiko\",\n    \"tyzan\",\n    \"taisei\",\n    \"tyhei\",\n    \"tadar\",\n    \"tsurugi\",\n    \"tenka\",\n    \"tenku\",\n    \"denko\",\n    \"toshi\",\n    \"tokka\",\n    \"hagan\",\n    \"hakusui\",\n    \"hakuto\",\n    \"hayate\",\n    \"hayabusa\",\n    \"hanni\",\n    \"hei\",\n    \"hiken\",\n    \"bizen\",\n    \"hynk\",\n    \"hideh\",\n    \"hiden\",\n    \"bakuya\",\n    \"huga\",\n    \"hiryu\",\n    \"fuwei\",\n    \"fugetsu\",\n    \"hukut\",\n    \"fudor\",\n    \"hekireki\",\n    \"bengara\",\n    \"horoh\",\n    \"hokuto\",\n    \"shigan\",\n    \"mikazuchi\",\n    \"mizuchi\",\n    \"mitsukage\",\n    \"mior\",\n    \"mu\",\n    \"mugen\",\n    \"musashi\",\n    \"mujo\",\n    \"musou\",\n    \"muchika\",\n    \"murakumo\",\n    \"murasame\",\n    \"meikyo\",\n    \"mejiro\",\n    \"yago\",\n    \"yakumo\",\n    \"yasha\",\n    \"yata\",\n    \"yanagi\",\n    \"yamato\",\n    \"yuki\",\n    \"yuzen\",\n    \"yuso\",\n    \"yumo\",\n    \"yoshikiri\",\n    \"rykiri\",\n    \"ranmaru\",\n    \"rikuzen\",\n    \"ryusei\",\n    \"ryo\",\n    \"rogen\",\n    \"reiki\",\n    \"roho\",\n    \"ai\",\n    \"aui\",\n    \"auba\",\n    \"akana\",\n    \"yoiyami\",\n    \"asagi\",\n    \"asai\",\n    \"aska\",\n    \"azuki\",\n    \"atori\",\n    \"amanei\",\n    \"amayori\",\n    \"ayame\",\n    \"anzu\",\n    \"koyuki\",\n    \"kyoka\",\n    \"isuzu\",\n    \"ichiku\",\n    \"iroha\",\n    \"uzura\",\n    \"uzuki\",\n    \"umi\",\n    \"ema\",\n    \"orka\",\n    \"kaeda\",\n    \"sarasa\",\n    \"kanami\",\n    \"kanon\",\n    \"karin\",\n    \"karei\",\n    \"karyn\",\n    \"kanna\",\n    \"kiko\",\n    \"kisaragi\",\n    \"kiri\",\n    \"kinsei\",\n    \"quina\",\n    \"kuko\",\n    \"kurumi\",\n    \"kurenai\",\n    \"kogoku\",\n    \"kokutan\",\n    \"kokuyo\",\n    \"kokoro\",\n    \"konoha\",\n    \"kohana\",\n    \"kohaku\",\n    \"sakuya\",\n    \"sazami\",\n    \"satsuki\",\n    \"sango\",\n    \"shiori\",\n    \"shigura\",\n    \"shisui\",\n    \"shizuku\",\n    \"shinome\",\n    \"shinobu\",\n    \"shimoki\",\n    \"shussu\",\n    \"shuraya\",\n    \"shiranui\",\n    \"shirayuki\",\n    \"shirayuri\",\n    \"shirome\",\n    \"suiren\",\n    \"suzu\",\n    \"suzukaze\",\n    \"suzuna\",\n    \"suzuran\",\n    \"sumira\",\n    \"tsumugi\",\n    \"sekirei\",\n    \"setsuka\",\n    \"sora\",\n    \"tamayori\",\n    \"chigusa\",\n    \"chidori\",\n    \"tsugumi\",\n    \"tsukumi\",\n    \"zutsuji\",\n    \"tsubaki\",\n    \"tsumi\",\n    \"tsura\",\n    \"tsuruba\",\n    \"tomae\",\n    \"torwa\",\n    \"nazuna\",\n    \"natsuki\",\n    \"nadeshiko\",\n    \"natori\",\n    \"ne-ne\",\n    \"nenoh\",\n    \"neyuki\",\n    \"nosuri\",\n    \"hasu\",\n    \"hazuki\",\n    \"hatsuharu\",\n    \"hatsuyuki\",\n    \"haruka\",\n    \"harusa\",\n    \"haruna\",\n    \"higana\",\n    \"hisui\",\n    \"hinagi\",\n    \"hinagetsu\",\n    \"hinata\",\n    \"hibari\",\n    \"hibiki\",\n    \"himawari\",\n    \"faera\",\n    \"fubuki\",\n    \"fuyoshi\",\n    \"yuzu\",\n    \"botania\",\n    \"honoka\",\n    \"madoka\",\n    \"mikagami\",\n    \"mikazuki\",\n    \"mika\",\n    \"mikoto\",\n    \"misaki\",\n    \"midori\",\n    \"minazuki\",\n    \"minami\",\n    \"minori\",\n    \"miyuki\",\n    \"mirei\",\n    \"mutsuki\",\n    \"maegi\",\n    \"mochi\",\n    \"momiji\",\n    \"moyoi\",\n    \"yayoi\",\n    \"yuka\",\n    \"yutsuji\",\n    \"yuna\",\n    \"yukina\",\n    \"yukine\",\n    \"yuzuki\",\n    \"yura\",\n    \"yuri\",\n    \"yomogi\",\n    \"rania\",\n    \"rinnia\",\n    \"lindora\",\n    \"lin\",\n    \"ruri\",\n    \"reika\",\n    \"rengenne\",\n    \"wakaba\",\n    \"utsuwaka\",\n    \"kai\",\n    \"kukir\",\n    \"kuro\",\n    \"goemon\",\n    \"koske\",\n    \"kotar\",\n    \"goro\",\n    \"kontro\",\n    \"sasuke\",\n    \"shimaru\",\n    \"shiro\",\n    \"tamar\",\n    \"tamon\",\n    \"tibbi\",\n    \"chamaru\",\n    \"tokoto\",\n    \"totetsu\",\n    \"baku\",\n    \"hutar\",\n    \"pochi\",\n    \"bonten\",\n    \"ryta\",\n    \"riku\",\n];\n"
  },
  {
    "path": "lib/tailscale_client/Cargo.toml",
    "content": "[package]\nname = \"tailscale_client\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [ \"Xe Iaso <me@xeiaso.net>\" ]\nrepository = \"https://github.com/Xe/waifud\"\nlicense = \"mit\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nserde_json = \"1\"\nthiserror = \"1\"\n\n[dependencies.chrono]\nversion = \"0.4\"\nfeatures = [ \"serde\" ]\n\n[dependencies.serde]\nversion = \"1\"\nfeatures = [ \"derive\" ]\n\n[dependencies.reqwest]\nversion = \"0.11\"\nfeatures = [ \"json\" ]"
  },
  {
    "path": "lib/tailscale_client/src/lib.rs",
    "content": "use chrono::prelude::*;\nuse serde::{Deserialize, Serialize};\n\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[error(\"error making request: {0}\")]\n    Reqwest(#[from] reqwest::Error),\n\n    #[error(\"error parsing json: {0}\")]\n    JSON(#[from] serde_json::Error),\n}\n\npub type Result<T = (), E = Error> = std::result::Result<T, E>;\n\n/// The Tailscale API Client. Each call will be its own method.\npub struct Client {\n    cli: reqwest::Client,\n    api_key: String,\n    tailnet: String,\n    base_url: String,\n}\n\n/// The minimal form of a Tailscale API key, only shows the ID.\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Key {\n    pub id: String,\n}\n\n/// Full information about a Tailscale API key.\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct KeyInfo {\n    pub id: String,\n    #[serde(skip_serializing)]\n    pub key: Option<String>,\n    pub created: DateTime<Utc>,\n    pub expires: DateTime<Utc>,\n    /// TODO(Xe): make this into a better value\n    pub capabilities: serde_json::Value,\n}\n\n#[test]\nfn test_keyinfo_full() {\n    let inp = r#\"{\n\t\"id\":           \"k123456CNTRL\",\n\t\"key\":          \"tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz\",\n\t\"created\":      \"2021-12-09T23:22:39Z\",\n\t\"expires\":      \"2022-03-09T23:22:39Z\",\n\t\"capabilities\": {\"devices\": {\"create\": {\"reusable\": false, \"ephemeral\": false}}}\n}\"#;\n    let _: KeyInfo = serde_json::from_str(inp).unwrap();\n}\n\n#[test]\nfn test_keyinfo_partial() {\n    let inp = r#\"{\n\t\"id\":           \"k123456CNTRL\",\n\t\"created\":      \"2021-12-09T23:22:39Z\",\n\t\"expires\":      \"2022-03-09T23:22:39Z\",\n\t\"capabilities\": {\"devices\": {\"create\": {\"reusable\": false, \"ephemeral\": false}}}\n}\"#;\n    let _: KeyInfo = serde_json::from_str(inp).unwrap();\n}\n\n#[derive(Debug, Clone, Deserialize, Serialize)]\npub struct Capabilities {\n    pub reusable: bool,\n    pub ephemeral: bool,\n    pub preauthorized: bool,\n    pub tags: Vec<String>,\n}\n\nimpl Client {\n    /// Maybe construct a new client with a given user agent string.\n    pub fn new(user_agent: String, api_key: String, tailnet: String) -> Result<Self> {\n        let cli = reqwest::Client::builder().user_agent(user_agent).build()?;\n\n        Ok(Client {\n            cli,\n            api_key,\n            tailnet,\n            base_url: \"https://api.tailscale.com\".to_string(),\n        })\n    }\n\n    /// List all active node authentication keys in the tailnet.\n    pub async fn list_keys(&self) -> Result<Vec<Key>> {\n        #[derive(Debug, Clone, Deserialize, Serialize)]\n        struct Wrapper {\n            keys: Vec<Key>,\n        }\n\n        let w: Wrapper = self\n            .cli\n            .get(&format!(\n                \"{}/api/v2/tailnet/{}/keys\",\n                self.base_url, self.tailnet\n            ))\n            .basic_auth(&self.api_key, None::<&String>)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?;\n\n        Ok(w.keys)\n    }\n\n    /// Creates a new machine authkey for the tailnet. This cannot be used\n    /// to create API keys.\n    pub async fn create_key(&self, caps: Capabilities) -> Result<KeyInfo> {\n        #[derive(Debug, Clone, Deserialize, Serialize)]\n        struct Outer {\n            capabilities: Inner,\n        }\n\n        #[derive(Debug, Clone, Deserialize, Serialize)]\n        struct Inner {\n            devices: Inner2,\n        }\n\n        #[derive(Debug, Clone, Deserialize, Serialize)]\n        struct Inner2 {\n            create: Capabilities,\n        }\n\n        let msg = Outer {\n            capabilities: Inner {\n                devices: Inner2 { create: caps },\n            },\n        };\n\n        Ok(self\n            .cli\n            .post(&format!(\n                \"{}/api/v2/tailnet/{}/keys\",\n                self.base_url, self.tailnet\n            ))\n            .basic_auth(&self.api_key, None::<&String>)\n            .json(&msg)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    /// Get detailed information for a given key by ID.\n    pub async fn key_info(&self, key_id: String) -> Result<KeyInfo> {\n        Ok(self\n            .cli\n            .get(&format!(\n                \"{}/api/v2/tailnet/{}/keys/{}\",\n                self.base_url, self.tailnet, key_id,\n            ))\n            .basic_auth(&self.api_key, None::<&String>)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    /// Delete a key by ID.\n    pub async fn delete_key(&self, key_id: String) -> Result {\n        self.cli\n            .delete(&format!(\n                \"{}/api/v2/tailnet/{}/keys/{}\",\n                self.base_url, self.tailnet, key_id,\n            ))\n            .basic_auth(&self.api_key, None::<&String>)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?;\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "lib/ts_localapi/Cargo.toml",
    "content": "[package]\nname = \"ts_localapi\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [ \"Xe Iaso <me@xeiaso.net>\" ]\nrepository = \"https://github.com/Xe/waifud\"\nlicense = \"mit\"\n\n[dependencies]\nhyper = \"0.14\"\nhyperlocal = \"0.8\"\nserde = { version = \"1\", features = [ \"derive\" ] }\nserde_json = \"1\"\nthiserror = \"1\""
  },
  {
    "path": "lib/ts_localapi/src/lib.rs",
    "content": "use hyper::{body::Buf, Body, Client, Request, StatusCode};\nuse hyperlocal::{UnixClientExt, Uri};\nuse serde::{Deserialize, Serialize};\nuse std::net::{IpAddr, SocketAddr};\n\n#[derive(Debug, thiserror::Error)]\npub enum Error {\n    #[error(\"hyper error: {0}\")]\n    Hyper(#[from] hyper::Error),\n\n    #[error(\"http error: {0}\")]\n    HTTP(#[from] hyper::http::Error),\n\n    #[error(\"json error: {0}\")]\n    JSON(#[from] serde_json::Error),\n\n    #[error(\"wanted status code {0}, but tailscaled returned status code {1}\")]\n    WrongStatusCode(StatusCode, StatusCode),\n}\n\n#[derive(Clone, PartialEq, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct WhoisResponse {\n    #[serde(rename = \"Node\")]\n    pub node: WhoisPeer,\n    #[serde(rename = \"UserProfile\")]\n    pub user_profile: User,\n}\n\npub async fn whois(ip_port: SocketAddr) -> Result<WhoisResponse, Error> {\n    let ip_port = if let SocketAddr::V6(ip_port) = ip_port {\n        let ip = ip_port\n            .ip()\n            .to_ipv4()\n            .map(|ip| IpAddr::V4(ip))\n            .unwrap_or(IpAddr::V6(ip_port.ip().clone()));\n        (ip, ip_port.port()).into()\n    } else {\n        ip_port\n    };\n    let url: hyper::Uri = Uri::new(\n        \"/var/run/tailscale/tailscaled.sock\",\n        &format!(\"/localapi/v0/whois?addr={ip_port}\"),\n    )\n    .into();\n    let client = Client::unix();\n\n    let req = Request::builder()\n        .uri(url)\n        .header(\"Host\", \"local-tailscaled.sock\")\n        .body(Body::empty())\n        .unwrap();\n\n    let resp = client.request(req).await?;\n    if !resp.status().is_success() {\n        return Err(Error::WrongStatusCode(StatusCode::OK, resp.status()));\n    }\n\n    let body = hyper::body::aggregate(resp).await?;\n\n    Ok(serde_json::from_reader(body.reader())?)\n}\n\n#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]\npub struct User {\n    #[serde(rename = \"ID\")]\n    pub id: u64,\n    #[serde(rename = \"LoginName\")]\n    pub login_name: String,\n    #[serde(rename = \"DisplayName\")]\n    pub display_name: String,\n    #[serde(rename = \"ProfilePicURL\")]\n    pub profile_pic_url: String,\n    #[serde(rename = \"Roles\")]\n    pub roles: Vec<Option<serde_json::Value>>,\n}\n\n#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct WhoisPeer {\n    #[serde(rename = \"ID\")]\n    pub id: i64,\n    #[serde(rename = \"StableID\")]\n    pub stable_id: String,\n    #[serde(rename = \"Name\")]\n    pub name: String,\n    #[serde(rename = \"User\")]\n    pub user: i64,\n    #[serde(rename = \"Key\")]\n    pub key: String,\n    #[serde(rename = \"KeyExpiry\")]\n    pub key_expiry: String,\n    #[serde(rename = \"Machine\")]\n    pub machine: String,\n    #[serde(rename = \"DiscoKey\")]\n    pub disco_key: String,\n    #[serde(rename = \"Addresses\")]\n    pub addresses: Vec<String>,\n    #[serde(rename = \"AllowedIPs\")]\n    pub allowed_ips: Vec<String>,\n    #[serde(rename = \"Endpoints\")]\n    pub endpoints: Vec<String>,\n    #[serde(rename = \"Hostinfo\")]\n    pub hostinfo: Hostinfo,\n    #[serde(rename = \"Created\")]\n    pub created: String,\n    #[serde(rename = \"PrimaryRoutes\")]\n    pub primary_routes: Option<Vec<String>>,\n    #[serde(rename = \"MachineAuthorized\")]\n    pub machine_authorized: Option<bool>,\n    #[serde(rename = \"Capabilities\")]\n    pub capabilities: Option<Vec<String>>,\n    #[serde(rename = \"ComputedName\")]\n    pub computed_name: String,\n    #[serde(rename = \"ComputedNameWithHost\")]\n    pub computed_name_with_host: String,\n}\n\n#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct Hostinfo {\n    #[serde(rename = \"OS\")]\n    pub os: Option<String>,\n    #[serde(rename = \"Hostname\")]\n    pub hostname: String,\n    #[serde(rename = \"RoutableIPs\")]\n    pub routable_ips: Option<Vec<String>>,\n    #[serde(rename = \"Services\")]\n    pub services: Vec<Service>,\n}\n\n#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct Service {\n    #[serde(rename = \"Proto\")]\n    pub proto: String,\n    #[serde(rename = \"Port\")]\n    pub port: i64,\n    #[serde(rename = \"Description\")]\n    pub description: Option<String>,\n}\n"
  },
  {
    "path": "scripts/.gitignore",
    "content": "*.qcow2*\n"
  },
  {
    "path": "scripts/metadata.json",
    "content": "{\"unstable\":{\"fname\":\"nixos-unstable-within-202307091255.qcow2\",\"sha256\":\"858f149120dc86d8bb1696b3d62f03c597a9706082709bd4220ac24d25640efe\"}}\n"
  },
  {
    "path": "scripts/mk-nixos-image.sh",
    "content": "#!/usr/bin/env nix-shell\n#! nix-shell -i bash -p nixos-generators -p qemu -p rsync -p jo\n\nset -ex\n\nDATE=\"$(date +%Y%m%d%H%M)\"\n\nNIX_PATH=nixpkgs=channel:nixos-unstable-small nixos-generate -f qcow -c ./nixos-image.nix -o ./nixos-unstable-within-${DATE}\n# NIX_PATH=nixpkgs=channel:nixos-21.11-small nixos-generate -f qcow -c ./nixos-image.nix -o ./nixos-unstable-within-${DATE}\n\nqemu-img convert -c -O qcow2 ./nixos-unstable-within-${DATE}/nixos.qcow2 nixos-unstable-within-${DATE}.qcow2\n# qemu-img convert -c -O qcow2 ./nixos-21.11-within-${DATE}/nixos.qcow2 nixos-21.11-within-${DATE}.qcow2\n\nsha256sum nixos-unstable-within-${DATE}.qcow2 > nixos-unstable-within-${DATE}.qcow2.sha256\n# sha256sum nixos-21.11-within-${DATE}.qcow2 > nixos-21.11-within-${DATE}.qcow2.sha256\n\nrsync -avz --progress *.qcow2* lufta:/srv/http/xena.greedo.xeserv.us/pkg/nixos/\n\nrm ./nixos-unstable-within-${DATE}\n# rm ./nixos-21.11-within-${DATE}\n\nrm -f metadata.json\ntouch metadata.json\njo -o metadata.json \\\n    unstable=$(jo \\\n                   fname=nixos-unstable-within-${DATE}.qcow2 \\\n                   sha256=$(cat nixos-unstable-within-${DATE}.qcow2.sha256 | cut -d' ' -f1))\n    # 21.11=$(jo \\\n    #             fname=nixos-21.11-within-${DATE}.qcow2 \\\n    #             sha256=$(cat nixos-unstable-within-${DATE}.qcow2.sha256 | cut -d' ' -f1)) \\\n\nrsync -avz --progress metadata.json lufta:/srv/http/xena.greedo.xeserv.us/pkg/nixos/\n"
  },
  {
    "path": "scripts/nixos-image.nix",
    "content": "{ lib, pkgs, ... }:\n\n{\n  boot.initrd.availableKernelModules =\n    [ \"ata_piix\" \"uhci_hcd\" \"virtio_pci\" \"sr_mod\" \"virtio_blk\" ];\n  boot.initrd.kernelModules = [ ];\n  boot.kernelModules = [ ];\n  boot.extraModulePackages = [ ];\n  boot.growPartition = true;\n  boot.kernelParams = [ \"console=ttyS0\" ];\n  boot.loader.grub.device = \"/dev/vda\";\n  boot.loader.timeout = 0;\n\n  fileSystems.\"/\" = {\n    device = \"/dev/disk/by-label/nixos\";\n    fsType = \"ext4\";\n    autoResize = true;\n  };\n\n  nix = {\n    package = pkgs.nixVersions.stable;\n    extraOptions = ''\n      experimental-features = nix-command flakes\n    '';\n\n    settings = {\n      auto-optimise-store = true;\n      sandbox = true;\n      substituters = [\n        \"https://xe.cachix.org\"\n        \"https://nix-community.cachix.org\"\n        \"https://cuda-maintainers.cachix.org\"\n        \"https://cache.floxdev.com?trusted=1\"\n        \"https://cache.garnix.io\"\n      ];\n      trusted-users = [ \"root\" \"cadey\" ];\n      trusted-public-keys = [\n        \"xe.cachix.org-1:kT/2G09KzMvQf64WrPBDcNWTKsA79h7+y2Fn2N7Xk2Y=\"\n        \"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\"\n        \"cuda-maintainers.cachix.org-1:0dq3bujKpuEPMCX6U4WylrUDZ9JyUG0VpVZa7CNfq5E=\"\n        \"flox-store-public-0:8c/B+kjIaQ+BloCmNkRUKwaVPFWkriSAd0JJvuDu4F0=\"\n        \"cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=\"\n      ];\n    };\n  };\n\n  systemd.services.\"within.website-first-run\" = {\n    description = \"bootstrap the first run of a NixOS machine on waifud\";\n    wantedBy = [ \"multi-user.target\" ];\n    after = [ \"network.target\" \"polkit.service\" ];\n    path = [ \"/run/current-system/sw/\" ];\n\n    script = with pkgs; ''\n      if ! [ -f /etc/nixos/configuration.nix ]; then\n        install -D ${./nixos-image.nix} /mnt/etc/nixos/configuration.nix\n      fi\n    '';\n  };\n\n  systemd.services.cloud-init.requires = lib.mkForce [ \"network.target\" ];\n\n  services.tailscale.enable = true;\n  services.openssh.enable = true;\n\n  services.cloud-init = {\n    enable = true;\n    ext4.enable = true;\n  };\n\n  users.motd = \"Welcome to waifud <3\";\n}\n"
  },
  {
    "path": "shell.nix",
    "content": "(import (\n  fetchTarball {\n    url = \"https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz\";\n    sha256 = \"0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2\"; }\n) {\n  src =  ./.;\n}).shellNix\n"
  },
  {
    "path": "src/admin/mod.rs",
    "content": "use crate::{\n    api::libvirt::Machine,\n    models::{Distro, Instance},\n    tailauth::Tailauth,\n    Config, Result, State,\n};\nuse axum::{extract::Path, Extension, Json};\nuse maud::{html, Markup, PreEscaped};\nuse rusqlite::params;\nuse std::sync::Arc;\nuse ts_localapi::User;\nuse uuid::Uuid;\nuse virt::{connect::Connect, domain::Domain};\n\nfn import_js(name: &str) -> PreEscaped<String> {\n    PreEscaped(format!(\n        r#\"<script type =\"module\">\nimport {{ Page }} from \"/static/js/{name}\";\n\nconst g = (name) => document.getElementById(name);\nconst r = (callback) => window.addEventListener(\"DOMContentLoaded\", callback);\nconst x = (elem) => {{\n    while (elem.lastChild) {{\n        elem.removeChild(elem.lastChild);\n    }}\n}};\n\nr(async () => {{\n  const page = await Page();\n  const root = g(\"app\");\n  x(root);\n  root.appendChild(page);\n}});\n</script>\"#\n    ))\n}\n\npub async fn config(Extension(config): Extension<Arc<Config>>, _: Tailauth) -> Json<Config> {\n    Json((*config).clone())\n}\n\npub fn base(\n    title: Option<String>,\n    crumbs: Option<&[(&str, Option<&str>)]>,\n    user_data: User,\n    body: Markup,\n) -> Markup {\n    let page_title = title.clone().unwrap_or(\"waifud\".to_string());\n    let title = title\n        .map(|s| format!(\"{s} - waifud\"))\n        .unwrap_or(\"waifud\".to_string());\n\n    let crumbs = if let Some(crumbs) = crumbs {\n        html! {\n            nav.breadcrumb.nav {\n                div.right {\n                    {(user_data.display_name)}\n                    \" \"\n                        img style=\"width:32px;height:32px\" src=(user_data.profile_pic_url);\n                }\n                ul {\n                    li { a href=\"/admin\" { \"waifud\" } }\n                    @for (name, link) in crumbs {\n                        li {\n                            @match link {\n                                Some(link) =>                                     a href=(link) {(name)},\n                                None => span aria-current=\"page\" {(name)},\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    } else {\n        html! {\n            nav.nav {\n                div.right {\n                    {(user_data.display_name)}\n                    \" \"\n                    img style=\"width:32px;height:32px\" src=(user_data.profile_pic_url);\n                }\n                a href=\"/admin\" {\"waifud\"}\n                \" \"\n                a href=\"/admin/distros\" {\"Distros\"}\n                \" \"\n                a href=\"/admin/instances\" {\"Instances\"}\n            }\n        }\n    };\n\n    html! {\n        (maud::DOCTYPE)\n        html {\n            head {\n                meta charset=\"utf-8\";\n                title {(title)}\n                meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\";\n                link rel=\"icon\" href=\"data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>\";\n                link rel=\"stylesheet\" type=\"text/css\" href=\"/static/css/xess.css\";\n            }\n            body.top {\n                main {\n                    (crumbs)\n                    br;\n                    h1 {(page_title)}\n                    br;\n                    (body);\n                    hr;\n                    footer {\n                        p {\n                            \"Powered with dokis by \"\n                            a href=\"https://github.com/Xe/waifud\" {\"waifud\"}\n                            \". ❤️\"\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\npub async fn instance_create(Tailauth(user, _): Tailauth) -> Markup {\n    base(\n        Some(\"Create instance\".to_string()),\n        Some(&[(\"Instances\", Some(\"/admin/instances\")), (\"Create\", None)]),\n        user,\n        html! {\n            (import_js(\"instance_create.js\"))\n            div #app {\n                \"Loading...\"\n            }\n        },\n    )\n}\n\npub async fn instance(\n    Extension(state): Extension<Arc<State>>,\n    Tailauth(user, _): Tailauth,\n    Path(id): Path<Uuid>,\n) -> Result<Markup> {\n    let conn = state.pool.get().await?;\n\n    let instance = Instance::from_uuid(&conn, id)?;\n\n    let conn = Connect::open(&format!(\"qemu+ssh://root@{}/system\", instance.host))?;\n    let machine: Option<Machine> = Domain::lookup_by_uuid_string(&conn, &id.to_string())\n        .ok()\n        .and_then(|dom| Machine::try_from(dom).ok());\n\n    Ok(base(\n        Some(instance.name.clone()),\n        Some(&[\n            (\"Instances\", Some(\"/admin/instances\")),\n            (&instance.name, None),\n        ]),\n        user,\n        html! {\n            (import_js(\"instance_detail.js\"))\n            table {\n                tr {\n                    th {\"Status\"}\n                    td {(instance.status)}\n                }\n                tr {\n                    th {\"IP Address\"}\n                    td {\n                        @if let Some(m) = machine {\n                            (m.addr.unwrap_or(\"\".to_string()))\n                        }\n                    }\n                }\n                tr {\n                    th {\"Host\"}\n                    td {(instance.host)}\n                }\n                tr {\n                    th {\"Memory\"}\n                    td {(instance.memory) \" MB\"}\n                }\n                tr {\n                    th {\"Disk size\"}\n                    td {(instance.disk_size) \" GB\"}\n                }\n                tr {\n                    th {\"ZVol name\"}\n                    td {(instance.zvol_name)}\n                }\n                tr {\n                    th {\"Distro\"}\n                    td {(instance.distro)}\n                }\n                tr {\n                    th {\"UUID\"}\n                    td #instance_id {(instance.uuid.to_string())}\n                }\n            }\n\n            h2 {\"Quick Actions\"}\n            div #app {\"Loading...\"}\n        },\n    ))\n}\n\npub async fn instances(\n    Extension(state): Extension<Arc<State>>,\n    Tailauth(user, _): Tailauth,\n) -> Result<Markup> {\n    let conn = state.pool.get().await?;\n\n    let mut result: Vec<Instance> = Vec::new();\n\n    let mut stmt = conn.prepare(\n        \"SELECT uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet FROM instances\",\n    )?;\n    let instances = stmt.query_map(params![], |row| {\n        Ok(Instance {\n            uuid: row.get(0)?,\n            name: row.get(1)?,\n            host: row.get(2)?,\n            mac_address: row.get(3)?,\n            memory: row.get(4)?,\n            disk_size: row.get(5)?,\n            zvol_name: row.get(6)?,\n            status: row.get(7)?,\n            distro: row.get(8)?,\n            join_tailnet: row.get(9)?,\n        })\n    })?;\n\n    for instance in instances {\n        result.push(instance?);\n    }\n\n    Ok(base(\n        Some(\"Instances\".to_string()),\n        Some(&[(\"Instances\", None)]),\n        user,\n        html! {\n            p{ a href=\"/admin/instances/create\" {\"Create a new instance\"} }\n            table {\n                tr {\n                    th {\"Name\"}\n                    th {\"Host\"}\n                    th {\"Memory\"}\n                    th {\"Disk\"}\n                    th {\"Distro\"}\n                    th {\"Status\"}\n                }\n                @for i in result {\n                    tr {\n                        td {a href={\"/admin/instances/\" (i.uuid.to_string())} {(i.name)}}\n                        td {(i.host)}\n                        td {(i.memory) \" MB\"}\n                        td {(i.disk_size) \" GB\"}\n                        td {(i.distro)}\n                        td {(i.status)}\n                    }\n                }\n            }\n        },\n    ))\n}\n\npub async fn home(\n    Extension(state): Extension<Arc<State>>,\n    Tailauth(user, _): Tailauth,\n) -> Result<Markup> {\n    let conn = state.pool.get().await?;\n\n    let mut stmt = conn.prepare(\n        \"\nWITH distro_count    ( val ) AS ( SELECT COUNT(*) FROM distros )\n   , instance_count  ( val ) AS ( SELECT COUNT(*) FROM instances )\n   , instance_memory ( amt ) AS ( SELECT SUM(memory) FROM instances )\nSELECT dc.val AS distros\n     , ic.val AS instances\n     , im.amt AS ram_use\nFROM distro_count    dc\n   , instance_count  ic\n   , instance_memory im\n\",\n    )?;\n\n    let (distro_count, instance_count, total_memory): (i32, i32, Option<i32>) =\n        stmt.query_row(params![], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;\n\n    Ok(base(\n        Some(\"Home\".to_string()),\n        None,\n        user.clone(),\n        html! {\n            p {\n                \"Hello \"\n                (user.login_name)\n                \"! I am tracking \"\n                (distro_count)\n                \" distribution image\"\n                @if distro_count != 1 {\n                    \"s\"\n                }\n                \", \"\n                (instance_count)\n                \" VM instance\"\n                @if instance_count != 1 {\n                    \"s\"\n                }\n                 \" that use a total of \"\n                (total_memory.unwrap_or(0))\n                \" megabytes of RAM.\"\n            }\n            p{ a href=\"/admin/instances/create\" {\"Create a new instance\"} }\n        },\n    ))\n}\n\npub async fn distro_list(\n    Extension(state): Extension<Arc<State>>,\n    Tailauth(user, _): Tailauth,\n) -> Result<Markup> {\n    let conn = state.pool.get().await?;\n\n    let mut stmt = conn.prepare(\n        \"SELECT name, download_url, sha256sum, min_size, format FROM distros ORDER BY name ASC\",\n    )?;\n    let iter = stmt.query_map(params![], |row| {\n        Ok(Distro {\n            name: row.get(0)?,\n            download_url: row.get(1)?,\n            sha256sum: row.get(2)?,\n            min_size: row.get(3)?,\n            format: row.get(4)?,\n        })\n    })?;\n    let mut result: Vec<Distro> = vec![];\n\n    for distro in iter {\n        result.push(distro.unwrap());\n    }\n\n    Ok(base(\n        Some(\"Distros\".to_string()),\n        Some(&[(\"Distros\", None)]),\n        user,\n        html! {\n            table {\n                tr {\n                    th {\"Name\"}\n                    th {\"Min. Size (gb)\"}\n                }\n                @for d in result {\n                    tr {\n                        td {(d.name)}\n                        td {(d.min_size)}\n                    }\n                }\n            }\n        },\n    ))\n}\n\npub async fn test_handler(Tailauth(user, _): Tailauth) -> Result<Markup> {\n    Ok(base(\n        Some(\"Test Page lol\".to_string()),\n        None,\n        user,\n        html! {\n            p {\"I'm baby tonx narwhal ennui crucifix taiyaki yr farm-to-table lomo locavore chillwave next level. Af palo santo bicycle rights try-hard gentrify jianbing viral heirloom actually sartorial fashion axe pickled artisan selvage cred. Celiac hammock sriracha yes plz, fit migas semiotics bruh shabby chic gluten-free chambray portland pug. Vice activated charcoal cornhole messenger bag enamel pin, put a bird on it blog ascot kale chips green juice sartorial twee retro. Try-hard hashtag umami leggings tote bag chillwave.\"}\n            h2 {\"Lumbersexual polaroid\"}\n            p {\n                \"Migas trust fund sriracha pop-up occupy. Chicharrones meggings bruh green juice squid. Brunch ennui umami fit gastropub 8-bit dreamcatcher. Bespoke portland pork belly vegan direct trade shoreditch austin franzen same +1 hoodie sustainable pickled celiac succulents. Lo-fi squid pok pok, chillwave master cleanse DIY tbh enamel pin gastropub iPhone yes plz lyft actually lumbersexual.\"\n            }\n            p {\n                \"Next level gastropub intelligentsia flannel tote bag, pug tilde lumbersexual poke mustache occupy. Seitan viral poutine messenger bag, echo park wayfarers af bruh poke distillery jianbing. Chillwave activated charcoal +1, disrupt shoreditch swag humblebrag lyft bushwick readymade same taxidermy kickstarter cold-pressed unicorn. Organic cloud bread polaroid tacos listicle man braid poutine chia skateboard fixie.\"\n            }\n        },\n    ))\n}\n"
  },
  {
    "path": "src/api/audit.rs",
    "content": "use crate::{models::AuditEvent, tailauth::Tailauth, Result, State};\nuse axum::{\n    extract::{Extension, Path},\n    Json,\n};\nuse std::sync::Arc;\n\n#[instrument(err, skip(state))]\npub async fn list(\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<Json<Vec<AuditEvent>>> {\n    let conn = state.pool.get().await?;\n\n    Ok(Json(AuditEvent::get_all(&conn)?))\n}\n\n#[instrument(err, skip(state))]\npub async fn list_for_instance(\n    Path(id): Path<uuid::Uuid>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<Json<Vec<AuditEvent>>> {\n    let conn = state.pool.get().await?;\n\n    Ok(Json(AuditEvent::get_for_instance(id, &conn)?))\n}\n"
  },
  {
    "path": "src/api/cloudinit.rs",
    "content": "use crate::{models::Instance, Error, State};\nuse axum::extract::{Extension, Path};\nuse rusqlite::params;\nuse serde::{Deserialize, Serialize};\nuse std::sync::Arc;\nuse uuid::Uuid;\n\n#[instrument(err)]\npub async fn user_data(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n) -> Result<String, Error> {\n    let conn = state.pool.get().await?;\n\n    Ok(conn.query_row(\n        \"SELECT user_data FROM cloudconfig_seeds WHERE uuid = ?1\",\n        params![id],\n        |row| Ok(row.get(0)?),\n    )?)\n}\n\n#[instrument(err)]\npub async fn meta_data(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n) -> Result<String, Error> {\n    let conn = state.pool.get().await?;\n\n    let hostname: String = conn.query_row(\n        \"SELECT name FROM instances WHERE uuid = ?1\",\n        params![id],\n        |row| row.get(0),\n    )?;\n    \n    conn.execute(\n        \"UPDATE instances SET status = ?1 WHERE uuid = ?2\",\n        params![\"running\", id],\n    )?;\n    let ins = Instance::from_uuid(&conn, id)?;\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"instance\", \"running\", serde_json::to_string(&ins)?],\n    )?;\n\n    Ok(format!(\n        \"instance-id: {}\nlocal-hostname: {}\",\n        id, hostname,\n    ))\n}\n\n#[instrument(err, skip(ts))]\npub async fn vendor_data(\n    Path(id): Path<Uuid>,\n    Extension(ts): Extension<Arc<tailscale_client::Client>>,\n    Extension(state): Extension<Arc<State>>,\n) -> Result<String, Error> {\n    let conn = state.pool.get().await?;\n\n    let i: Instance = Instance::from_uuid(&conn, id)?;\n\n    if i.join_tailnet {\n        let key_info = ts\n            .create_key(tailscale_client::Capabilities {\n                reusable: false,\n                ephemeral: true,\n                preauthorized: true,\n                tags: vec![\"tag:vm\".to_string()],\n            })\n            .await?;\n\n        conn.execute(\n            \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n            params![\n                \"tailnet authkey\",\n                \"create\",\n                serde_json::to_string(&key_info)?\n            ],\n        )?;\n\n        if i.distro == \"ubuntu-20.04\".to_string() || i.distro == \"ubuntu-22.04\".to_string() {\n            Ok(format!(\"#cloud-config\\n{}\", serde_yaml::to_string(&CloudConfig{\n                write_files: vec![\n                    File{\n                        owner: \"root:root\".to_string(),\n                        path: \"/etc/update-motd.d/69-waifud\".to_string(),\n                        permissions: \"0755\".to_string(),\n                        content: \"#!/bin/sh\\n#\\n# This file is written by waifud.\\necho \\\"\\\"\\necho \\\"Welcome to waifud <3\\\"\\n\".to_string(),\n                    },\n                ],\n                runcmd: vec![\n                    vec![\"sh\".into(), \"-c\".into(), \"curl -fsSL https://tailscale.com/install.sh | sh\".into()],\n                    vec![\"systemctl\".into(), \"enable\".into(), \"--now\".into(), \"tailscaled.service\".into()],\n                    vec![\"tailscale\".into(), \"up\".into(), \"--authkey\".into(), key_info.key.unwrap(), \"--ssh\".into(), \"--advertise-tags=tag:vm\".into()],\n                    vec![\"apt\".into(), \"install\".into(), \"-y\".into(), \"systemd-container\".into()]\n                ],\n            })?))\n        } else {\n            Ok(format!(\"#cloud-config\\n{}\", serde_yaml::to_string(&CloudConfig{\n                write_files: vec![\n                    File{\n                        owner: \"root:root\".into(),\n                        path: \"/etc/update-motd.d/69-waifud\".into(),\n                        permissions: \"0755\".into(),\n                        content: \"#!/bin/sh\\n#\\n# This file is written by waifud.\\necho \\\"\\\"\\necho \\\"Welcome to waifud <3\\\"\\n\".into(),\n                    },\n                ],\n                runcmd: vec![\n                    vec![\"sh\".into(), \"-c\".into(), \"curl -fsSL https://tailscale.com/install.sh | sh\".into()],\n                    vec![\"systemctl\".into(), \"enable\".into(), \"--now\".into(), \"tailscaled.service\".into()],\n                    vec![\"tailscale\".into(), \"up\".into(), \"--authkey\".into(), key_info.key.unwrap(), \"--ssh\".into()],\n                ],\n            })?))\n        }\n    } else {\n        Ok(include_str!(\"./vendor-data\").to_string())\n    }\n}\n\n#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct CloudConfig {\n    #[serde(rename = \"write_files\")]\n    pub write_files: Vec<File>,\n    pub runcmd: Vec<Vec<String>>,\n}\n\n#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct File {\n    pub owner: String,\n    pub path: String,\n    pub permissions: String,\n    pub content: String,\n}\n"
  },
  {
    "path": "src/api/distros.rs",
    "content": "use crate::{models::Distro, tailauth::Tailauth, Result, State};\nuse axum::{\n    extract::{Extension, Path},\n    Json,\n};\nuse rusqlite::params;\nuse std::sync::Arc;\n\n#[instrument(err)]\npub async fn create(\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n    Json(distro): Json<Distro>,\n) -> Result<Json<Distro>> {\n    let conn = state.pool.get().await?;\n\n    let mut distro = distro;\n    if distro.format == \"\".to_string() {\n        distro.format = \"waifud://qcow2\".into();\n    }\n\n    {\n        let d = distro.clone();\n        conn.execute(\n            \"INSERT INTO distros\n                   ( name\n                   , download_url\n                   , sha256sum\n                   , min_size\n                   , format\n                   )\n             VALUES\n                 ( ?1\n                 , ?2\n                 , ?3\n                 , ?4\n                 , ?5\n                 )\",\n            params![d.name, d.download_url, d.sha256sum, d.min_size, d.format],\n        )?;\n    }\n\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"distro\", \"create\", serde_json::to_string(&distro)?],\n    )?;\n\n    Ok(Json(distro))\n}\n\n#[instrument(err)]\npub async fn update(\n    Path(name): Path<String>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n    Json(distro): Json<Distro>,\n) -> Result<Json<Distro>> {\n    let conn = state.pool.get().await?;\n\n    let mut distro = distro;\n    if distro.format == \"\".to_string() {\n        distro.format = \"waifud://qcow2\".into();\n    }\n\n    let d = distro.clone();\n    conn.execute(\n        \"\nINSERT INTO\n  distros( name\n         , download_url\n         , sha256sum\n         , min_size\n         , format\n         )\nVALUES ( ?5\n       , ?1\n       , ?2\n       , ?3\n       , ?4\n       )\nON CONFLICT DO\n  UPDATE SET download_url=?1\n           , sha256sum=?2\n           , min_size=?3\n           , format=?4\n\",\n        params![d.download_url, d.sha256sum, d.min_size, d.format, d.name],\n    )?;\n\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"distro\", \"update\", serde_json::to_string(&d)?],\n    )?;\n\n    Ok(Json(distro))\n}\n\n#[instrument(err)]\npub async fn delete(\n    Extension(state): Extension<Arc<State>>,\n    Path(name): Path<String>,\n    _: Tailauth,\n) -> Result<()> {\n    let conn = state.pool.get().await?;\n\n    let d = Distro::from_name(&conn, name)?;\n    conn.execute(\"DELETE FROM distros WHERE name = ?1\", params![d.name])?;\n\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"distro\", \"update\", serde_json::to_string(&d)?],\n    )?;\n\n    Ok(())\n}\n\n#[instrument(err)]\npub async fn get(\n    Extension(state): Extension<Arc<State>>,\n    Path(name): Path<String>,\n    _: Tailauth,\n) -> Result<Json<Distro>> {\n    let conn = state.pool.get().await?;\n\n    Ok(Json(conn.query_row(\n        \"SELECT\n           name\n         , download_url\n         , sha256sum\n         , min_size\n         , format\n         FROM distros\n         WHERE name = ?1\",\n        params![name],\n        |row| {\n            Ok(Distro {\n                name: row.get(0)?,\n                download_url: row.get(1)?,\n                sha256sum: row.get(2)?,\n                min_size: row.get(3)?,\n                format: row.get(4)?,\n            })\n        },\n    )?))\n}\n\n#[instrument(err)]\npub async fn list(\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<Json<Vec<Distro>>> {\n    let conn = state.pool.get().await?;\n\n    let mut stmt = conn.prepare(\n        \"SELECT name, download_url, sha256sum, min_size, format FROM distros ORDER BY name ASC\",\n    )?;\n    let iter = stmt.query_map(params![], |row| {\n        Ok(Distro {\n            name: row.get(0)?,\n            download_url: row.get(1)?,\n            sha256sum: row.get(2)?,\n            min_size: row.get(3)?,\n            format: row.get(4)?,\n        })\n    })?;\n    let mut result: Vec<Distro> = vec![];\n\n    for distro in iter {\n        result.push(distro.unwrap());\n    }\n\n    Ok(Json(result))\n}\n"
  },
  {
    "path": "src/api/instances.rs",
    "content": "use crate::{\n    api::libvirt::Machine,\n    libvirt::{random_mac, NewInstance},\n    models::{Distro, Instance},\n    tailauth::Tailauth,\n    Config, Error, State,\n};\nuse axum::{\n    extract::{Extension, Path},\n    Json,\n};\nuse rusqlite::params;\nuse std::{\n    convert::TryFrom, net::SocketAddr, os::unix::prelude::ExitStatusExt, process::ExitStatus,\n    sync::Arc, time::Duration,\n};\nuse tokio::{net::lookup_host, process::Command, task::spawn_blocking, time::sleep};\nuse uuid::Uuid;\nuse virt::{connect::Connect, domain::Domain};\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn reinit(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<(), Error> {\n    let conn = state.pool.get().await?;\n\n    let mut i = Instance::from_uuid(&conn, id)?;\n\n    let nuke: Result<(), Error> = {\n        let host = i.host.clone();\n        let id = i.uuid.clone();\n\n        spawn_blocking(move || {\n            let conn = Connect::open(&format!(\"qemu+ssh://root@{}/system\", host))?;\n\n            let dom = Domain::lookup_by_uuid_string(&conn, &id.to_string())?;\n\n            dom.destroy()?;\n            Ok(())\n        })\n        .await?\n    };\n    nuke?;\n\n    sleep(Duration::from_millis(500)).await;\n\n    debug!(\"rolling back zvol\");\n    let output = Command::new(\"ssh\")\n        .args([\n            \"-lroot\",\n            &i.host,\n            \"zfs\",\n            \"rollback\",\n            &format!(\"{}@init\", i.zvol_name),\n        ])\n        .output()\n        .await?;\n    if output.status != ExitStatus::from_raw(0) {\n        let stderr = String::from_utf8(output.stderr).unwrap();\n        return Err(Error::CantRollbackZvol(\n            i.host.clone(),\n            \"init\".to_string(),\n            stderr,\n        ));\n    }\n\n    let nuke: Result<(), Error> = {\n        let host = i.host.clone();\n        let id = i.uuid.clone();\n\n        spawn_blocking(move || {\n            let conn = Connect::open(&format!(\"qemu+ssh://root@{}/system\", host))?;\n\n            let dom = Domain::lookup_by_uuid_string(&conn, &id.to_string())?;\n\n            dom.create()?;\n            Ok(())\n        })\n        .await?\n    };\n    nuke?;\n\n    i.status = \"reinit\".to_string();\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"instance\", \"reinit\", serde_json::to_string(&i)?],\n    )?;\n\n    Ok(())\n}\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn delete(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<(), Error> {\n    let conn = state.pool.get().await?;\n\n    let i = Instance::from_uuid(&conn, id)?;\n\n    let nuke: Result<(), Error> = {\n        let host = i.host.clone();\n        let id = i.uuid.clone();\n\n        spawn_blocking(move || {\n            let conn = Connect::open(&format!(\"qemu+ssh://root@{}/system\", host))?;\n\n            let dom = Domain::lookup_by_uuid_string(&conn, &id.to_string())?;\n\n            dom.destroy()?;\n            dom.undefine_flags(virt::sys::VIR_DOMAIN_UNDEFINE_NVRAM)?;\n            Ok(())\n        })\n        .await?\n    };\n    nuke?;\n\n    sleep(Duration::from_millis(500)).await;\n\n    debug!(\"destroying zvol\");\n    let output = Command::new(\"ssh\")\n        .args([\"-lroot\", &i.host, \"zfs\", \"destroy\", \"-rf\", &i.zvol_name])\n        .output()\n        .await?;\n    if output.status != ExitStatus::from_raw(0) {\n        let stderr = String::from_utf8(output.stderr).unwrap();\n        return Err(Error::CantDeleteZvol(i.host.clone(), stderr));\n    }\n\n    conn.execute(\"DELETE FROM instances WHERE uuid = ?1\", params![id])?;\n    conn.execute(\"DELETE FROM cloudconfig_seeds WHERE uuid = ?1\", params![id])?;\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"instance\", \"delete\", serde_json::to_string(&i)?],\n    )?;\n\n    Ok(())\n}\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn get_machine(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<Json<Machine>, Error> {\n    let conn = state.pool.get().await?;\n\n    let mut stmt = conn.prepare(\"SELECT host FROM instances WHERE uuid = ?1\")?;\n    let host: String = stmt.query_row(params![id], |row| row.get(0))?;\n\n    let conn = Connect::open(&format!(\"qemu+ssh://root@{}/system\", host))?;\n\n    let dom = Domain::lookup_by_uuid_string(&conn, &id.to_string())?;\n    Ok(Json(Machine::try_from(dom)?))\n}\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn hard_reboot(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<(), Error> {\n    let conn = state.pool.get().await?;\n\n    let i = Instance::from_uuid(&conn, id)?;\n    let vc = Connect::open(&format!(\"qemu+ssh://root@{}/system\", i.host))?;\n\n    let dom = Domain::lookup_by_uuid_string(&vc, &id.to_string())?;\n\n    dom.destroy()?;\n    dom.create()?;\n\n    conn.execute(\n        \"UPDATE instances SET status = ?1 WHERE uuid = ?2\",\n        params![\"rebooting\", id],\n    )?;\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"instance\", \"hard reboot\", serde_json::to_string(&i)?],\n    )?;\n\n    Ok(())\n}\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn shutdown(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<(), Error> {\n    let conn = state.pool.get().await?;\n\n    let i = Instance::from_uuid(&conn, id)?;\n    let vc = Connect::open(&format!(\"qemu+ssh://root@{}/system\", i.host))?;\n\n    let dom = Domain::lookup_by_uuid_string(&vc, &id.to_string())?;\n    dom.shutdown()?;\n\n    conn.execute(\n        \"UPDATE instances SET status = ?1 WHERE uuid = ?2\",\n        params![\"off\", id],\n    )?;\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"instance\", \"shutdown\", serde_json::to_string(&i)?],\n    )?;\n\n    Ok(())\n}\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn start(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<(), Error> {\n    let conn = state.pool.get().await?;\n\n    let i = Instance::from_uuid(&conn, id)?;\n    let vc = Connect::open(&format!(\"qemu+ssh://root@{}/system\", i.host))?;\n\n    let dom = Domain::lookup_by_uuid_string(&vc, &id.to_string())?;\n    dom.create()?;\n\n    conn.execute(\n        \"UPDATE instances SET status = ?1 WHERE uuid = ?2\",\n        params![\"starting\", id],\n    )?;\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"instance\", \"start\", serde_json::to_string(&i)?],\n    )?;\n\n    Ok(())\n}\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn reboot(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<(), Error> {\n    let conn = state.pool.get().await?;\n\n    let i = Instance::from_uuid(&conn, id)?;\n    let vc = Connect::open(&format!(\"qemu+ssh://root@{}/system\", i.host))?;\n\n    let dom = Domain::lookup_by_uuid_string(&vc, &id.to_string())?;\n    dom.reboot(0)?;\n\n    conn.execute(\n        \"UPDATE instances SET status = ?1 WHERE uuid = ?2\",\n        params![\"rebooting\", id],\n    )?;\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"instance\", \"reboot\", serde_json::to_string(&i)?],\n    )?;\n\n    Ok(())\n}\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn get_by_name(\n    Path(name): Path<String>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<Json<Instance>, Error> {\n    let conn = state.pool.get().await?;\n\n    Ok(Json(Instance::from_name(&conn, name)?))\n}\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn get(\n    Path(id): Path<Uuid>,\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<Json<Instance>, Error> {\n    let conn = state.pool.get().await?;\n\n    Ok(Json(Instance::from_uuid(&conn, id)?))\n}\n\n#[instrument(err)]\n#[axum_macros::debug_handler]\npub async fn list(\n    Extension(state): Extension<Arc<State>>,\n    _: Tailauth,\n) -> Result<Json<Vec<Instance>>, Error> {\n    let conn = state.pool.get().await?;\n\n    let mut result: Vec<Instance> = Vec::new();\n\n    let mut stmt = conn.prepare(\n        \"SELECT uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet FROM instances\",\n    )?;\n    let instances = stmt.query_map(params![], |row| {\n        Ok(Instance {\n            uuid: row.get(0)?,\n            name: row.get(1)?,\n            host: row.get(2)?,\n            mac_address: row.get(3)?,\n            memory: row.get(4)?,\n            disk_size: row.get(5)?,\n            zvol_name: row.get(6)?,\n            status: row.get(7)?,\n            distro: row.get(8)?,\n            join_tailnet: row.get(9)?,\n        })\n    })?;\n\n    for instance in instances {\n        result.push(instance?);\n    }\n\n    Ok(Json(result))\n}\n\n#[instrument(err)]\npub async fn create(\n    Extension(state): Extension<Arc<State>>,\n    Extension(config): Extension<Arc<Config>>,\n    _: Tailauth,\n    Json(details): Json<NewInstance>,\n) -> Result<Json<Instance>, Error> {\n    let id = Uuid::new_v4();\n\n    let addrs: Vec<SocketAddr> = lookup_host(details.host.clone() + \":22\".into())\n        .await?\n        .collect();\n    if addrs.len() == 0 {\n        return Err(Error::HostDoesntExist(details.host));\n    }\n\n    let conn = state.pool.get().await?;\n\n    let distro = conn.query_row(\n        \"SELECT name, download_url, sha256sum, min_size, format FROM distros WHERE name = ?1\",\n        params![details.distro.clone()],\n        |row| {\n            Ok(Distro {\n                name: row.get(0)?,\n                download_url: row.get(1)?,\n                sha256sum: row.get(2)?,\n                min_size: row.get(3)?,\n                format: row.get(4)?,\n            })\n        },\n    )?;\n\n    let details = NewInstance {\n        name: details.name.or(rotbart::unique_monster()),\n        memory_mb: details.memory_mb.or(Some(512)),\n        host: details.host.clone(),\n        disk_size_gb: details.disk_size_gb.or(Some(distro.min_size)),\n        zvol_prefix: details.zvol_prefix.or(Some(\"rpool/safe/vms\".into())),\n        distro: distro.name.clone(),\n        sata: details.sata.or(Some(false)),\n        cpus: details.cpus.or(Some(2)),\n        user_data: details\n            .user_data\n            .or(Some(include_str!(\"../../var/xe-base.yaml\").into())),\n        join_tailnet: details.join_tailnet.clone(),\n    };\n\n    let mac_addr = random_mac();\n    let zvol_name = format!(\n        \"{}/{}\",\n        details.zvol_prefix.clone().unwrap(),\n        details.name.clone().unwrap()\n    );\n\n    let ins = Instance {\n        uuid: id,\n        name: details.name.clone().unwrap(),\n        host: details.host.clone(),\n        memory: details.memory_mb.unwrap(),\n        disk_size: details.disk_size_gb.unwrap(),\n        mac_address: mac_addr.clone(),\n        zvol_name: zvol_name.clone(),\n        status: \"init\".into(),\n        distro: details.distro.clone(),\n        join_tailnet: details.join_tailnet.clone(),\n    };\n\n    {\n        let ins = ins.clone();\n        conn.execute(\n            \"INSERT INTO instances(uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)\",\n            params![\n                ins.uuid,\n                ins.name,\n                ins.host,\n                mac_addr,\n                ins.memory,\n                ins.disk_size,\n                zvol_name,\n                ins.status,\n                ins.distro,\n                ins.join_tailnet,\n            ],\n        )?;\n        conn.execute(\n            \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n            params![\"instance\", \"create\", serde_json::to_string(&ins)?],\n        )?;\n\n        conn.execute(\n            \"INSERT INTO cloudconfig_seeds(uuid, user_data) VALUES (?1, ?2)\",\n            params![id.clone(), details.user_data.clone().unwrap()],\n        )?;\n    }\n\n    drop(conn);\n\n    {\n        let ins = ins.clone();\n        tokio::spawn(async move {\n            if let Err(why) = make_instance(config, state, details, ins, distro, mac_addr, id).await\n            {\n                error!(\"can't make instance: {}\", why);\n            }\n        });\n    }\n\n    Ok(Json(ins))\n}\n\n#[instrument(ret, level = \"debug\", err, skip(config, state, details, id, mac_addr))]\nasync fn make_instance(\n    config: Arc<Config>,\n    state: Arc<State>,\n    details: NewInstance,\n    ins: Instance,\n    distro: Distro,\n    mac_addr: String,\n    id: Uuid,\n) -> Result<(), Error> {\n    let conn = state.pool.get().await?;\n    let mut ins = ins.clone();\n\n    debug!(\"name: {}\", details.name.as_ref().unwrap());\n\n    debug!(\"checking if image exists\");\n    let output = Command::new(\"ssh\")\n        .args([\n            \"-oStrictHostKeyChecking=accept-new\",\n            &details.host.clone(),\n            \"stat\",\n            &format!(\"$HOME/.cache/within/mkvm/qcow2/{}\", distro.sha256sum),\n        ])\n        .output()\n        .await?;\n    if output.status != ExitStatus::from_raw(0) {\n        debug!(\"downloading image\");\n        ins.status = \"downloading image\".into();\n        conn.execute(\n            \"UPDATE instances SET status = ?1 WHERE uuid = ?2\",\n            params![ins.status, id],\n        )?;\n        conn.execute(\n            \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n            params![\"instance\", ins.status, serde_json::to_string(&ins)?],\n        )?;\n        let output = Command::new(\"ssh\")\n            .args([\n                &details.host.clone(),\n                \"wget\",\n                \"-O\",\n                &format!(\"$HOME/.cache/within/mkvm/qcow2/{}\", distro.sha256sum),\n                &distro.download_url,\n            ])\n            .output()\n            .await?;\n        if output.status != ExitStatus::from_raw(0) {\n            let stderr = String::from_utf8(output.stderr).unwrap();\n\n            Command::new(\"ssh\")\n                .args([\n                    \"-oStrictHostKeyChecking=accept-new\",\n                    &details.host.clone(),\n                    \"rm\",\n                    &format!(\"$HOME/.cache/within/mkvm/qcow2/{}\", distro.sha256sum),\n                ])\n                .status()\n                .await?;\n\n            return Err(Error::CantDownloadImage(\n                distro.download_url.clone(),\n                stderr,\n            ));\n        }\n    }\n    debug!(\"making zvol\");\n    let output = Command::new(\"ssh\")\n        .args([\n            \"-lroot\",\n            \"-oStrictHostKeyChecking=accept-new\",\n            &details.host.clone(),\n            \"zfs\",\n            \"create\",\n            \"-V\",\n            &format!(\"{}G\", details.disk_size_gb.unwrap()),\n            &format!(\n                \"{}/{}\",\n                details.zvol_prefix.as_ref().unwrap(),\n                &details.name.as_ref().unwrap()\n            ),\n        ])\n        .output()\n        .await?;\n    if output.status != ExitStatus::from_raw(0) {\n        let stderr = String::from_utf8(output.stderr).unwrap();\n        return Err(Error::CantMakeZvol(details.host.clone(), stderr));\n    }\n    debug!(\"hydrating zvol\");\n    ins.status = \"hydrating zvol\".into();\n    conn.execute(\n        \"UPDATE instances SET status = ?1 WHERE uuid = ?2\",\n        params![ins.status, id],\n    )?;\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"instance\", ins.status, serde_json::to_string(&ins)?],\n    )?;\n    let output = Command::new(\"ssh\")\n        .args([\n            \"-lroot\",\n            \"-oStrictHostKeyChecking=accept-new\",\n            &details.host.clone(),\n            \"qemu-img\",\n            \"convert\",\n            \"-O\",\n            \"raw\",\n            &format!(\"/home/cadey/.cache/within/mkvm/qcow2/{}\", distro.sha256sum),\n            &format!(\n                \"/dev/zvol/{}/{}\",\n                details.zvol_prefix.as_ref().unwrap(),\n                &details.name.as_ref().unwrap()\n            ),\n        ])\n        .output()\n        .await?;\n    if output.status != ExitStatus::from_raw(0) {\n        let stderr = String::from_utf8(output.stderr).unwrap();\n        return Err(Error::CantHydrateZvol(details.host.clone(), stderr));\n    }\n    debug!(\"making init snapshot\");\n    let output = Command::new(\"ssh\")\n        .args([\n            \"-lroot\",\n            \"-oStrictHostKeyChecking=accept-new\",\n            &details.host.clone(),\n            \"zfs\",\n            \"snapshot\",\n            &format!(\n                \"{}/{}@init\",\n                details.zvol_prefix.as_ref().unwrap(),\n                &details.name.as_ref().unwrap()\n            ),\n        ])\n        .output()\n        .await?;\n    if output.status != ExitStatus::from_raw(0) {\n        let stderr = String::from_utf8(output.stderr).unwrap();\n        return Err(Error::CantMakeInitSnapshot(details.host.clone(), stderr));\n    }\n\n    let mut buf: Vec<u8> = vec![];\n\n    debug!(\"rendering xml\");\n    crate::templates::base_xml(\n        &mut buf,\n        details.name.clone().unwrap(),\n        id.to_string(),\n        mac_addr.clone(),\n        details.zvol_prefix.clone().unwrap(),\n        details.sata.unwrap(),\n        details.memory_mb.unwrap() * 1024,\n        details.cpus.unwrap(),\n        format!(\"{}/api/cloudinit/{}/\", config.clone().base_url, id),\n        config.qemu_path.clone(),\n    )?;\n\n    let buf = String::from_utf8(buf).unwrap();\n    trace!(\"libvirt xml:\\n{}\", buf);\n\n    let addr: Result<(), Error> = {\n        let details = details.clone();\n        spawn_blocking(move || {\n            debug!(\"connecting to host\");\n            let lc = Connect::open(&format!(\"qemu+ssh://root@{}/system\", details.host.clone()))?;\n\n            debug!(\"defining domain\");\n            let dom = Domain::define_xml(&lc, &buf)?;\n\n            debug!(\"starting domain\");\n            dom.create()?;\n            Ok(())\n        })\n        .await?\n    };\n    let addr = addr?;\n\n    debug!(\"ip: {:?}\", addr);\n\n    ins.status = \"waiting for cloud-init\".into();\n    conn.execute(\n        \"UPDATE instances SET status = ?1 WHERE uuid = ?2\",\n        params![ins.status, id],\n    )?;\n    conn.execute(\n        \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n        params![\"instance\", ins.status, serde_json::to_string(&ins)?],\n    )?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/api/libvirt.rs",
    "content": "use crate::{Config, Error, Result};\nuse axum::{extract::Extension, Json};\nuse serde::{Deserialize, Serialize};\nuse std::{convert::TryFrom, sync::Arc};\nuse virt::{connect::Connect, domain::Domain};\n\n#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct Machine {\n    pub name: String,\n    pub host: String,\n    pub active: bool,\n    pub uuid: String,\n    pub addr: Option<String>,\n    pub memory_megs: u64,\n    pub cpus: u32,\n}\n\nimpl TryFrom<Domain> for Machine {\n    type Error = Error;\n\n    fn try_from(dom: Domain) -> Result<Self, Self::Error> {\n        let addr: Option<String> = if dom.is_active()? {\n            let mut addr: Vec<String> = dom\n                .interface_addresses(virt_sys::VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE, 0)?\n                .into_iter()\n                .map(|iface| iface.addrs.clone())\n                .filter(|addrs| addrs.get(0).is_some())\n                .map(|addrs| addrs.get(0).unwrap().clone().addr)\n                .collect();\n\n            if addr.get(0).is_none() {\n                None\n            } else {\n                Some(addr.swap_remove(0))\n            }\n        } else {\n            None\n        };\n\n        let info = dom.get_info()?;\n        let max_memory = dom.get_max_memory()? / 1024;\n        let conn = dom.get_connect()?;\n        let host = conn.get_hostname()?;\n\n        Ok(Machine {\n            name: dom.get_name()?,\n            host,\n            active: dom.is_active()?,\n            uuid: dom.get_uuid_string()?,\n            addr,\n            memory_megs: max_memory,\n            cpus: info.nr_virt_cpu,\n        })\n    }\n}\n\n#[instrument(err, skip(cfg))]\npub async fn get_machines(Extension(cfg): Extension<Arc<Config>>) -> Result<Json<Vec<Machine>>> {\n    let mut result = Vec::new();\n    for host in &cfg.hosts {\n        result.extend_from_slice(&list_all_vms(\n            &format!(\"qemu+ssh://root@{}/system\", host),\n            host.to_string(),\n        )?);\n    }\n    Ok(Json(result))\n}\n\n#[instrument(skip(uri), err)]\nfn list_all_vms(uri: &str, host: String) -> Result<Vec<Machine>> {\n    debug!(\"connecting to {}: {}\", host, uri);\n    let mut conn = Connect::open(uri)?;\n    let mut result = vec![];\n\n    for dom in conn.list_all_domains(0)? {\n        result.push(Machine::try_from(dom)?);\n    }\n    conn.close()?;\n\n    Ok(result)\n}\n"
  },
  {
    "path": "src/api/mod.rs",
    "content": "pub mod audit;\npub mod cloudinit;\npub mod distros;\npub mod instances;\npub mod libvirt;\n"
  },
  {
    "path": "src/api/vendor-data",
    "content": "#cloud-config\nwrite_files:\n- owner: root:root\n  path: /etc/update-motd.d/69-waifud\n  permissions: '0755'\n  content: |\n    #!/bin/sh\n    #\n    # This file is written by waifud.\n    echo \"\"\n    echo \"Welcome to waifud <3\"\n"
  },
  {
    "path": "src/bin/unique-monster.rs",
    "content": "use clap::Parser;\nuse names::Name;\n\n#[derive(Parser)]\n#[clap(author, version, about, long_about = None)]\nstruct Cli {\n    #[clap(short, long, default_value = \"5\")]\n    count: usize,\n\n    #[clap(short, long)]\n    add_numbers: bool,\n}\n\nfn main() {\n    let cli = Cli::parse();\n\n    let generator = names::Generator::new(\n        &rotbart::COMBINED_ADJ,\n        &rotbart::COMBINED_NOUN,\n        if cli.add_numbers {\n            Name::Numbered\n        } else {\n            Name::Plain\n        },\n    );\n    generator\n        .take(cli.count)\n        .for_each(|name| println!(\"{name}\"));\n}\n"
  },
  {
    "path": "src/bin/waifuctl.rs",
    "content": "#![deny(missing_docs)]\n\n//! waifuctl lets you manage VM instances on waifud.\n\n#[macro_use]\nextern crate tracing;\n\nuse chrono::prelude::*;\nuse clap::{Args, Parser, Subcommand};\nuse clap_complete::{generate, Shell};\nuse serde::{Deserialize, Serialize};\nuse serde_dhall::StaticType;\nuse std::{\n    convert::TryInto,\n    fs,\n    io::{self, stdout, Write},\n    path::PathBuf,\n    process::exit,\n    time::Duration,\n};\nuse tabular::{row, Table};\nuse waifud::{\n    client::Client,\n    libvirt::NewInstance,\n    models::{Distro, Instance},\n    Error, Result,\n};\n\n#[derive(Debug, Parser)]\n#[clap(author, version, about, long_about = None)]\n#[clap(propagate_version = true)]\n/// waifuctl lets you manage VM instances on waifud.\nstruct Opt {\n    /// waifud host to connect to, formatted as a http/https URL\n    #[clap(short = 'H', long)]\n    pub host: Option<String>,\n\n    #[clap(subcommand)]\n    cmd: Command,\n}\n\n#[derive(Deserialize, Serialize, Debug, StaticType, Clone)]\nstruct Config {\n    /// waifud host to connect to, formatted as a http/https URL\n    pub host: String,\n\n    /// Default cloudconfig to preload into every VM\n    pub userdata: String,\n}\n\n#[derive(Subcommand, Debug)]\nenum ConfigCmd {\n    /// Shows current config\n    Show,\n\n    /// Set the waifud host to an arbitrary URL\n    SetHost {\n        /// The waifud host\n        url: String,\n    },\n\n    /// Set the default cloudconfig added to instances\n    SetUserdata,\n}\n\n#[derive(Subcommand, Debug)]\nenum Command {\n    /// Manage audit logs\n    Audit {\n        /// Format all audit logs in JSON\n        #[clap(long)]\n        json: bool,\n    },\n    /// Manage waifuctl configuration\n    Config {\n        #[clap(subcommand)]\n        cmd: ConfigCmd,\n    },\n    /// List all instances\n    List,\n    Create(CreateOpts),\n    /// Delete an instance by name\n    Delete {\n        /// Instance name\n        name: String,\n    },\n    Distro {\n        #[clap(subcommand)]\n        cmd: DistroCmd,\n    },\n    /// Reset a VM back to factory settings\n    Reinit {\n        /// Instance name\n        name: String,\n    },\n    /// Turn an instance on\n    Start {\n        /// Instance name\n        name: String,\n    },\n    /// Turn an instance off\n    Shutdown {\n        /// Instance name\n        name: String,\n    },\n    /// Manually trigger instance reboot\n    Reboot {\n        /// Instance name\n        name: String,\n\n        /// Unsafely force reboot\n        #[clap(short, long)]\n        hard: bool,\n    },\n    /// Utilities to help with managing the waifud project\n    Utils {\n        #[clap(subcommand)]\n        cmd: UtilsCmd,\n    },\n}\n\n/// Create a new instance\n#[derive(Args, Debug)]\nstruct CreateOpts {\n    /// Instance name, leave blank to autogenerate\n    #[clap(short, long)]\n    name: Option<String>,\n\n    /// Memory in megabytes\n    #[clap(short, long, default_value = \"512\")]\n    memory: i32,\n\n    /// CPU cores\n    #[clap(short, long, default_value = \"2\")]\n    cpus: i32,\n\n    /// Host to put the VM on\n    #[clap(short = 'H', long)]\n    host: String,\n\n    /// Disk size in GB, leave blank to use distribution default\n    #[clap(short = 's', long = \"disk-size\")]\n    disk_size: Option<i32>,\n\n    /// ZFS dataset to put the VM disk in\n    #[clap(short, long = \"zvol\", default_value = \"rpool/local/vms\")]\n    zvol_prefix: String,\n\n    /// File containing cloud-init user data, if not set will default to configured value\n    #[clap(short, long)]\n    user_data: Option<PathBuf>,\n\n    /// Distribution to use\n    #[clap(short, long)]\n    distro: String,\n\n    /// Automagically join the tailnet\n    #[clap(short, long)]\n    join_tailnet: bool,\n}\n\nimpl TryInto<NewInstance> for CreateOpts {\n    type Error = anyhow::Error;\n\n    fn try_into(self) -> Result<NewInstance, anyhow::Error> {\n        let user_data = match self.user_data {\n            Some(user_data) => Some(fs::read_to_string(user_data)?),\n            None => None,\n        };\n\n        Ok(NewInstance {\n            name: self.name,\n            memory_mb: Some(self.memory),\n            cpus: Some(self.cpus),\n            host: self.host,\n            disk_size_gb: self.disk_size,\n            zvol_prefix: Some(self.zvol_prefix),\n            distro: self.distro,\n            sata: Some(false),\n            user_data,\n            join_tailnet: self.join_tailnet,\n        })\n    }\n}\n\n/// Manage distribution images in waifud\n#[derive(Subcommand, Debug)]\nenum DistroCmd {\n    /// Create a new base distro snapshot\n    Create(CreateDistroOpts),\n    /// Delete a distro image\n    Delete { name: String },\n    /// List all distros\n    List {\n        /// Show more information\n        #[clap(short)]\n        verbose: bool,\n    },\n    /// Scrapes current versions for distributions\n    Scrape,\n    /// Updates a base distro snapshot\n    Update(CreateDistroOpts),\n}\n\n/// Defines a base distro snapshot for waifud to use\n#[derive(Args, Debug)]\nstruct CreateDistroOpts {\n    /// Distribution name, include the version as a suffix\n    #[clap(short, long)]\n    pub name: String,\n\n    /// Download URL for the qcow2 base snapshot\n    #[clap(short, long = \"download-url\")]\n    pub download_url: String,\n\n    /// The sha256 of the qcow2 base snapshot\n    #[clap(short, long = \"sha256\")]\n    pub sha256sum: String,\n\n    /// The minimum size of a VM created from this snapshot (gigabytes)\n    #[clap(short, long)]\n    pub min_size: i32,\n\n    /// The format of the disk image\n    #[clap(short, long, default_value = \"waifud://qcow2\")]\n    pub format: String,\n}\n\nimpl Into<Distro> for CreateDistroOpts {\n    fn into(self) -> Distro {\n        Distro {\n            name: self.name,\n            download_url: self.download_url,\n            sha256sum: self.sha256sum,\n            min_size: self.min_size,\n            format: self.format,\n        }\n    }\n}\n\n#[derive(Subcommand, Debug)]\nenum UtilsCmd {\n    /// Generate shell completions\n    Completions {\n        #[clap(value_parser)]\n        shell: Shell,\n    },\n    /// Generate manpages to a given folder\n    Manpage { path: PathBuf },\n}\n\nasync fn list_instances(cli: Client) -> Result {\n    let instances = cli.list_instances().await?;\n\n    let mut table = Table::new(\"{:>}  {:<}  {:<}  {:<}  {:<}  {:<}  {:<}\");\n    table.add_row(row!(\n        \"name\", \"host\", \"distro\", \"memory\", \"ip\", \"status\", \"id\"\n    ));\n    for instance in instances {\n        let m = cli.get_instance_machine(instance.uuid).await;\n\n        table.add_row(row!(\n            instance.name,\n            instance.host,\n            instance.distro,\n            instance.memory,\n            match m {\n                Ok(m) => m.addr.unwrap_or(\"\".into()),\n                Err(_) => \"\".to_string(),\n            },\n            instance.status,\n            instance.uuid,\n        ));\n    }\n\n    println!(\"{}\", table);\n\n    Ok(())\n}\n\nasync fn wait_until_status<T>(cli: &Client, i: Instance, want: T) -> Result\nwhere\n    T: Into<String>,\n{\n    let want = want.into();\n    let mut i = i.clone();\n\n    loop {\n        i = cli.get_instance(i.uuid).await?;\n        io::stdout().flush()?;\n        print!(\n            \"{}: {}                                        \\r\",\n            i.name, i.status\n        );\n        if i.status == want {\n            break;\n        }\n\n        tokio::time::sleep(Duration::from_millis(1000)).await;\n    }\n\n    io::stdout().flush()?;\n    print!(\"\\n\");\n    Ok(())\n}\n\nasync fn start_instance(cli: Client, name: String) -> Result {\n    let i = cli.get_instance_by_name(name).await?;\n\n    cli.start_instance(i.uuid).await?;\n\n    wait_until_status(&cli, i.clone(), \"running\").await?;\n    println!(\"{} is running\", i.name);\n\n    Ok(())\n}\n\nasync fn shutdown_instance(cli: Client, name: String) -> Result {\n    let i = cli.get_instance_by_name(name).await?;\n\n    cli.shutdown_instance(i.uuid).await?;\n\n    println!(\"shut down {}\", i.name);\n\n    Ok(())\n}\n\nasync fn reboot_instance(cli: Client, name: String, hard: bool) -> Result {\n    let i = cli.get_instance_by_name(name).await?;\n\n    if hard {\n        cli.hard_reboot_instance(i.uuid).await\n    } else {\n        cli.reboot_instance(i.uuid).await\n    }?;\n\n    wait_until_status(&cli, i, \"running\").await?;\n\n    Ok(())\n}\n\n#[instrument(ret, level = \"debug\", err, skip(cli))]\nasync fn create_instance(cli: Client, cfg: Config, opts: CreateOpts) -> Result {\n    let mut ni: NewInstance = opts.try_into()?;\n\n    if ni.user_data.is_none() {\n        ni.user_data = Some(cfg.userdata);\n    }\n\n    let i = cli.create_instance(ni).await?;\n\n    println!(\"created instance {} on {}\", i.name, i.host);\n\n    wait_until_status(&cli, i.clone(), \"running\").await?;\n\n    let m = cli.get_instance_machine(i.uuid).await?;\n\n    println!(\n        \"\\r{}: {}: IP address: {}\",\n        i.name,\n        i.status,\n        m.addr.unwrap()\n    );\n\n    Ok(())\n}\n\nasync fn delete_instance(cli: Client, name: String) -> Result {\n    let i = cli.get_instance_by_name(name.clone()).await;\n\n    match i {\n        Ok(i) => cli.delete_instance(i.uuid).await?,\n        Err(why) => {\n            eprintln!(\"no instance named {} was found: {}\", name, why);\n            return Err(Error::InstanceDoesntExist(name));\n        }\n    };\n\n    Ok(())\n}\n\nasync fn reinit_instance(cli: Client, name: String) -> Result<()> {\n    let i = cli.get_instance_by_name(name.clone()).await?;\n    cli.reinit_instance(i.uuid).await?;\n\n    Ok(())\n}\n\nasync fn create_distro(cli: Client, opts: CreateDistroOpts) -> Result {\n    let d: Distro = opts.into();\n    let d = cli.create_distro(d).await?;\n    println!(\"created {}\", d.name);\n\n    Ok(())\n}\n\nasync fn update_distro(cli: Client, opts: CreateDistroOpts) -> Result {\n    if let Err(why) = cli.get_distro(opts.name.clone()).await {\n        println!(\"can't get distro {}: {}\", opts.name, why);\n        exit(1);\n    }\n\n    let d: Distro = opts.into();\n    let d = cli.update_distro(d).await?;\n    println!(\"created {}\", d.name);\n\n    Ok(())\n}\n\nasync fn scrape_distros(cli: Client) -> Result {\n    let distros = waifud::scrape::get_all().await?;\n    for distro in distros {\n        cli.update_distro(distro.clone()).await?;\n        println!(\"updated {}\", distro.name);\n    }\n\n    Ok(())\n}\n\nasync fn list_distros(cli: Client, verbose: bool) -> Result {\n    let distros = cli.list_distros().await?;\n\n    if verbose {\n        let mut table = Table::new(\"{:>}  {:<}  {:<}  {:<}\");\n        table.add_row(row!(\"name\", \"min size\", \"sha256\", \"url\"));\n        for distro in distros {\n            table.add_row(row!(\n                distro.name,\n                distro.min_size,\n                distro.sha256sum,\n                distro.download_url,\n            ));\n        }\n\n        println!(\"{}\", table);\n    } else {\n        let mut table = Table::new(\"{:<}  {:<}\");\n        table.add_row(row!(\"name\", \"disk GB\"));\n        distros.into_iter().for_each(|d| {\n            table.add_row(row!(d.name, d.min_size.to_string()));\n        });\n        println!(\"{}\", table);\n    }\n\n    Ok(())\n}\n\nasync fn delete_distro(cli: Client, name: String) -> Result<()> {\n    cli.delete_distro(name).await?;\n    Ok(())\n}\n\nasync fn audit_list(cli: Client, json: bool) -> Result<()> {\n    let logs = cli.audit_logs().await?;\n\n    if json {\n        serde_json::to_writer(stdout(), &logs)?;\n        return Ok(());\n    }\n\n    let mut table = Table::new(\"{:>}  {:<}  {:<}  {:<}\");\n    table.add_row(row!(\"timestamp\", \"kind\", \"name\", \"op\"));\n\n    for log in logs {\n        let ts = NaiveDateTime::from_timestamp(log.ts, 0);\n        table.add_row(row!(\n            ts.to_string(),\n            log.kind,\n            log.name.unwrap_or(\"\".into()),\n            log.op\n        ));\n    }\n\n    println!(\"{}\", table);\n\n    Ok(())\n}\n\nfn config_show(cfg: Config) -> Result {\n    println!(\"waifud host: {}\", cfg.host);\n    println!(\"default cloudconfig:\\n\\n{}\", cfg.userdata);\n\n    Ok(())\n}\n\nfn config_set_host(cfg: Config, url: String) -> Result {\n    let mut cfg = cfg.clone();\n\n    cfg.host = url.clone();\n\n    let mut fname = dirs::config_dir().unwrap();\n    fname.push(\"xeserv\");\n    let _ = fs::create_dir_all(&fname);\n    fname.push(\"waifuctl\");\n    fname.set_extension(\"dhall\");\n\n    let mut fout = fs::File::create(&fname).unwrap();\n    let cfg = serde_dhall::serialize(&cfg)\n        .static_type_annotation()\n        .to_string()?;\n    fout.write_all(cfg.as_bytes())?;\n\n    println!(\"set host to {} in {}\", url, fname.to_str().unwrap());\n    Ok(())\n}\n\nfn config_set_userdata(cfg: Config) -> Result {\n    let userdata = edit::edit(&cfg.userdata)?;\n    let mut cfg = cfg.clone();\n    cfg.userdata = userdata;\n\n    let mut fname = dirs::config_dir().unwrap();\n    fname.push(\"xeserv\");\n    let _ = fs::create_dir_all(&fname);\n    fname.push(\"waifuctl\");\n    fname.set_extension(\"dhall\");\n\n    let mut fout = fs::File::create(&fname).unwrap();\n    let cfg = serde_dhall::serialize(&cfg)\n        .static_type_annotation()\n        .to_string()?;\n    fout.write_all(cfg.as_bytes())?;\n\n    println!(\"wrote default cloudconfig to {}\", fname.to_str().unwrap());\n\n    Ok(())\n}\n\nfn utils_completions(shell: Shell) -> Result {\n    let cmd = clap::Command::new(\"waifuctl\");\n    let mut cmd = Opt::augment_args(cmd);\n\n    generate(\n        shell,\n        &mut cmd,\n        \"waifuctl\".to_string(),\n        &mut std::io::stdout(),\n    );\n\n    Ok(())\n}\n\nfn utils_gen_manpage(path: PathBuf) -> Result {\n    let cmd = clap::Command::new(\"waifuctl\");\n    let cmd = Opt::augment_args(cmd);\n\n    let man = clap_mangen::Man::new(cmd.clone());\n    let mut buffer: Vec<u8> = Default::default();\n    man.render(&mut buffer)?;\n    std::fs::write(path.join(\"waifuctl.1\"), buffer)?;\n\n    for scmd in cmd.get_subcommands() {\n        let man = clap_mangen::Man::new(scmd.clone());\n        let mut buffer: Vec<u8> = Default::default();\n        man.render(&mut buffer)?;\n\n        std::fs::write(\n            path.join(&format!(\"waifuctl-{}.1\", scmd.get_name())),\n            buffer,\n        )?;\n\n        if scmd.has_subcommands() {\n            for sscmd in scmd.get_subcommands() {\n                let man = clap_mangen::Man::new(sscmd.clone());\n                let mut buffer: Vec<u8> = Default::default();\n                man.render(&mut buffer)?;\n\n                std::fs::write(\n                    path.join(&format!(\n                        \"waifuctl-{}-{}.1\",\n                        scmd.get_name(),\n                        sscmd.get_name()\n                    )),\n                    buffer,\n                )?;\n            }\n        }\n    }\n\n    Ok(())\n}\n\n#[tokio::main]\nasync fn main() -> Result<()> {\n    tracing_subscriber::fmt::init();\n\n    let mut opt = Opt::parse();\n\n    let cfg = {\n        let mut fname = dirs::config_dir().unwrap();\n        fname.push(\"xeserv\");\n        let _ = fs::create_dir_all(&fname);\n        fname.push(\"waifuctl\");\n        fname.set_extension(\"dhall\");\n\n        if opt.host.is_none() {\n            if let Err(_) = fs::metadata(&fname) {\n                let mut fout = fs::File::create(&fname).unwrap();\n                let cfg = serde_dhall::serialize(&Config {\n                    host: \"http://[::]:23818\".into(),\n                    userdata: include_str!(\"../../var/base.yaml\").to_string(),\n                })\n                .static_type_annotation()\n                .to_string()?;\n                fout.write_all(cfg.as_bytes())?;\n            }\n        }\n\n        let cfg = serde_dhall::from_file(&fname).parse::<Config>()?;\n        debug!(\"config: {:?}\", cfg);\n\n        if cfg.host.len() == 0 {\n            println!(\"welcome to waifud, you may want to run `waifuctl config set-host` to point waifuctl to your waifud server\");\n        }\n\n        cfg\n    };\n    if let None = opt.host {\n        opt.host = Some(cfg.host.clone());\n    }\n\n    debug!(\"{:?}\", opt);\n\n    let cli = Client::new(opt.host.unwrap())?;\n\n    if let Err(why) = match opt.cmd {\n        Command::Audit { json } => audit_list(cli, json).await,\n        Command::Distro { cmd } => match cmd {\n            DistroCmd::Create(opts) => create_distro(cli, opts).await,\n            DistroCmd::Delete { name } => delete_distro(cli, name).await,\n            DistroCmd::List { verbose } => list_distros(cli, verbose).await,\n            DistroCmd::Scrape => scrape_distros(cli).await,\n            DistroCmd::Update(opts) => update_distro(cli, opts).await,\n        },\n        Command::List => list_instances(cli).await,\n        Command::Create(opts) => create_instance(cli, cfg, opts).await,\n        Command::Delete { name } => delete_instance(cli, name).await,\n        Command::Reboot { name, hard } => reboot_instance(cli, name, hard).await,\n        Command::Reinit { name } => reinit_instance(cli, name).await,\n        Command::Start { name } => start_instance(cli, name).await,\n        Command::Shutdown { name } => shutdown_instance(cli, name).await,\n        Command::Config { cmd } => match cmd {\n            ConfigCmd::Show => config_show(cfg),\n            ConfigCmd::SetHost { url } => config_set_host(cfg, url),\n            ConfigCmd::SetUserdata => config_set_userdata(cfg),\n        },\n        Command::Utils { cmd } => match cmd {\n            UtilsCmd::Completions { shell } => utils_completions(shell),\n            UtilsCmd::Manpage { path } => utils_gen_manpage(path),\n        },\n    } {\n        eprintln!(\"OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The code monkeys at our headquarters are working VEWY HAWD to fix this!\");\n        eprintln!(\"{}\", why);\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/build.rs",
    "content": "use ructe::{Result, Ructe};\n\nfn main() -> Result<()> {\n    Ructe::from_env()?.compile_templates(\"templates\")\n}\n"
  },
  {
    "path": "src/client/mod.rs",
    "content": "use crate::{\n    api::libvirt::Machine,\n    libvirt::NewInstance,\n    models::{AuditEvent, Distro, Instance},\n    Result,\n};\nuse reqwest::header;\nuse std::time::Duration;\nuse url::Url;\nuse uuid::Uuid;\n\npub struct Client {\n    base_url: Url,\n    cli: reqwest::Client,\n}\n\nimpl Client {\n    pub fn new(base_url: String) -> Result<Self> {\n        let mut headers = header::HeaderMap::new();\n        headers.insert(\n            header::USER_AGENT,\n            header::HeaderValue::from_str(crate::APPLICATION_NAME)?,\n        );\n\n        let cli = reqwest::Client::builder()\n            .default_headers(headers)\n            .connect_timeout(Duration::from_millis(500))\n            .build()?;\n\n        Ok(Client {\n            base_url: Url::parse(&base_url)?,\n            cli,\n        })\n    }\n\n    pub async fn audit_logs(&self) -> Result<Vec<AuditEvent>> {\n        let mut u = self.base_url.clone();\n        u.set_path(\"/api/v1/auditlogs\");\n        Ok(self\n            .cli\n            .get(u)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn create_instance(&self, ni: NewInstance) -> Result<Instance> {\n        let mut u = self.base_url.clone();\n        u.set_path(\"/api/v1/instances\");\n        Ok(self\n            .cli\n            .post(u)\n            .json(&ni)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn delete_instance(&self, id: Uuid) -> Result {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/instances/{}\", id));\n        self.cli.delete(u).send().await?.error_for_status()?;\n        Ok(())\n    }\n\n    pub async fn reinit_instance(&self, id: Uuid) -> Result {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/instances/{}/reinit\", id));\n        self.cli.post(u).send().await?.error_for_status()?;\n        Ok(())\n    }\n\n    pub async fn list_instances(&self) -> Result<Vec<Instance>> {\n        let mut u = self.base_url.clone();\n        u.set_path(\"/api/v1/instances\");\n        Ok(self\n            .cli\n            .get(u)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn get_instance(&self, id: Uuid) -> Result<Instance> {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/instances/{}\", id));\n        Ok(self\n            .cli\n            .get(u)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn get_instance_by_name(&self, name: String) -> Result<Instance> {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/instances/name/{}\", name));\n        Ok(self\n            .cli\n            .get(u)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn get_instance_machine(&self, id: Uuid) -> Result<Machine> {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/instances/{}/machine\", id));\n        Ok(self\n            .cli\n            .get(u)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn shutdown_instance(&self, id: Uuid) -> Result<()> {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/instances/{}/shutdown\", id));\n        self.cli.post(u).send().await?.error_for_status()?;\n        Ok(())\n    }\n\n    pub async fn start_instance(&self, id: Uuid) -> Result<()> {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/instances/{}/start\", id));\n        self.cli.post(u).send().await?.error_for_status()?;\n        Ok(())\n    }\n\n    pub async fn hard_reboot_instance(&self, id: Uuid) -> Result<()> {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/instances/{}/hardreboot\", id));\n        self.cli.post(u).send().await?.error_for_status()?;\n        Ok(())\n    }\n\n    pub async fn reboot_instance(&self, id: Uuid) -> Result<()> {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/instances/{}/reboot\", id));\n        self.cli.post(u).send().await?.error_for_status()?;\n        Ok(())\n    }\n\n    pub async fn create_distro(&self, d: Distro) -> Result<Distro> {\n        let mut u = self.base_url.clone();\n        u.set_path(\"/api/v1/distros\");\n        Ok(self\n            .cli\n            .post(u)\n            .json(&d)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn list_distros(&self) -> Result<Vec<Distro>> {\n        let mut u = self.base_url.clone();\n        u.set_path(\"/api/v1/distros\");\n        Ok(self\n            .cli\n            .get(u)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn update_distro(&self, d: Distro) -> Result<Distro> {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/distros/{}\", d.name));\n        Ok(self\n            .cli\n            .post(u)\n            .json(&d)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn get_distro(&self, name: String) -> Result<Distro> {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/distros/{}\", name));\n        Ok(self\n            .cli\n            .get(u)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?)\n    }\n\n    pub async fn delete_distro(&self, name: String) -> Result {\n        let mut u = self.base_url.clone();\n        u.set_path(&format!(\"/api/v1/distros/{}\", name));\n        self.cli.delete(u).send().await?.error_for_status()?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse std::{fmt, net::IpAddr};\n\n#[derive(Clone, Serialize, Deserialize)]\npub struct Config {\n    #[serde(rename = \"baseURL\")]\n    pub base_url: String,\n    pub hosts: Vec<String>,\n    #[serde(rename = \"bindHost\")]\n    pub bind_host: IpAddr,\n    pub port: u16,\n    #[serde(rename = \"rpoolBase\")]\n    pub rpool_base: String,\n    #[serde(rename = \"qemuPath\")]\n    pub qemu_path: String,\n    #[serde(skip_serializing)]\n    pub tailscale: Tailscale,\n}\n\nimpl fmt::Debug for Config {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"Config()\")\n    }\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Tailscale {\n    #[serde(rename = \"apiKey\")]\n    pub api_key: String,\n    pub tailnet: String,\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "#[macro_use]\nextern crate tracing;\n\nuse axum::{\n    http::StatusCode,\n    response::{IntoResponse, Response},\n};\nuse bb8::Pool;\nuse bb8_rusqlite::RusqliteConnectionManager;\nuse hyper::header::InvalidHeaderValue;\nuse rusqlite::Connection;\nuse std::{env, fmt, net::AddrParseError};\n\npub const APPLICATION_NAME: &str = concat!(env!(\"CARGO_PKG_NAME\"), \"/\", env!(\"CARGO_PKG_VERSION\"),);\n\npub fn establish_connection() -> Result<Connection> {\n    let database_url = env::var(\"DATABASE_URL\").unwrap_or(\"./var/waifud.db\".to_string());\n    Ok(Connection::open(&database_url)?)\n}\n\npub type Result<T = (), E = Error> = std::result::Result<T, E>;\n\npub mod admin;\npub mod api;\npub mod client;\npub mod config;\npub mod libvirt;\npub mod migrate;\npub mod models;\npub mod scrape;\npub mod tailauth;\n\npub use config::Config;\n\npub struct State {\n    pub pool: Pool<RusqliteConnectionManager>,\n}\n\nimpl fmt::Debug for State {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"State()\")\n    }\n}\n\nimpl State {\n    pub async fn new() -> Result<Self> {\n        let mgr = RusqliteConnectionManager::new(\n            env::var(\"DATABASE_URL\").unwrap_or(\"./var/waifud.db\".to_string()),\n        );\n        let pool = bb8::Pool::builder().build(mgr).await?;\n        Ok(State { pool })\n    }\n}\n\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[error(\"libvirt error: {0}\")]\n    Libvirt(#[from] virt::error::Error),\n\n    #[error(\"dhall parsing error: {0}\")]\n    Dhall(#[from] serde_dhall::Error),\n\n    #[error(\"json parsing error: {0}\")]\n    Json(#[from] serde_json::Error),\n\n    #[error(\"yaml error: {0}\")]\n    YAML(#[from] serde_yaml::Error),\n\n    #[error(\"database error: {0}\")]\n    SQLite(#[from] rusqlite::Error),\n\n    #[error(\"database pool error: {0}\")]\n    SQLitePool(#[from] bb8_rusqlite::Error),\n\n    #[error(\"internal tokio error: {0}\")]\n    TokioJoin(#[from] tokio::task::JoinError),\n\n    #[error(\"hyper error: {0}\")]\n    Hyper(#[from] hyper::Error),\n\n    #[error(\"can't convert HTTP header to string: {0}\")]\n    HTTPHeaderToString(#[from] axum::http::header::ToStrError),\n\n    #[error(\"address parse error: {0}\")]\n    AddrParse(#[from] AddrParseError),\n\n    #[error(\"io error: {0}\")]\n    IO(#[from] std::io::Error),\n\n    #[error(\"reqwest error: {0}\")]\n    Reqwest(#[from] reqwest::Error),\n\n    #[error(\"url error: {0}\")]\n    URL(#[from] url::ParseError),\n\n    #[error(\"tailscale error: {0}\")]\n    Tailscale(#[from] tailscale_client::Error),\n\n    #[error(\"other error: {0}\")]\n    Catchall(String),\n\n    #[error(\"{0}\")]\n    Anyhow(#[from] anyhow::Error),\n\n    #[error(\"hex decode error: {0}\")]\n    Hex(#[from] hex::FromHexError),\n\n    #[error(\"tailscaled localapi error: {0}\")]\n    TailscaledLocalAPI(#[from] ts_localapi::Error),\n\n    #[error(\"invalid header value: {0}\")]\n    InvalidHTTPHeader(#[from] InvalidHeaderValue),\n\n    // Application errors\n    #[error(\"host {0} doesn't exist\")]\n    HostDoesntExist(String),\n\n    #[error(\"instance {0} doesn't exist\")]\n    InstanceDoesntExist(String),\n\n    #[error(\"can't download {0}:\\n\\n{1}\")]\n    CantDownloadImage(String, String),\n\n    #[error(\"can't create zfs zvol on {0}:\\n\\n{1}\")]\n    CantMakeZvol(String, String),\n\n    #[error(\"can't delete zfs zvol on {0}:\\n\\n{1}\")]\n    CantDeleteZvol(String, String),\n\n    #[error(\"can't rollback zfs zvol to snapshot {1} on {0}:\\n\\n{2}\")]\n    CantRollbackZvol(String, String, String),\n\n    #[error(\"can't hydrate zfs zvol on {0}:\\n\\n{1}\")]\n    CantHydrateZvol(String, String),\n\n    #[error(\"can't create zfs init snapshot on {0}:\\n\\n{1}\")]\n    CantMakeInitSnapshot(String, String),\n\n    #[error(\"internal middleware logic error\")]\n    BadMiddlewareStack,\n\n    #[error(\"insufficient authorization to perform this action\")]\n    Unauthorized,\n\n    #[error(\"can't make token: {0}\")]\n    CantMakeToken(String),\n}\n\nimpl<E> From<bb8::RunError<E>> for Error\nwhere\n    E: std::error::Error + Send + 'static,\n{\n    fn from(err: bb8::RunError<E>) -> Self {\n        Self::Catchall(format!(\"{}\", err))\n    }\n}\n\nimpl IntoResponse for Error {\n    fn into_response(self) -> Response {\n        let interm: (StatusCode, String) = self.into();\n        interm.into_response()\n    }\n}\n\nimpl Into<(StatusCode, String)> for Error {\n    fn into(self) -> (StatusCode, String) {\n        match self {\n            Error::Unauthorized => (\n                StatusCode::UNAUTHORIZED,\n                \"you lack authorization\".to_string(),\n            ),\n            Error::Libvirt(why) => (StatusCode::INTERNAL_SERVER_ERROR, why.message().to_string()),\n            Error::Dhall(why) => (StatusCode::BAD_REQUEST, format!(\"{}\", why)),\n            Error::SQLite(err) => match err {\n                rusqlite::Error::QueryReturnedNoRows => {\n                    (StatusCode::NOT_FOUND, \"404 not found\".into())\n                }\n                _ => (StatusCode::INTERNAL_SERVER_ERROR, format!(\"{}\", err)),\n            },\n            _ => (StatusCode::INTERNAL_SERVER_ERROR, format!(\"{}\", self)),\n        }\n    }\n}\n\ninclude!(concat!(env!(\"OUT_DIR\"), \"/templates.rs\"));\n"
  },
  {
    "path": "src/libvirt.rs",
    "content": "use mac_address::MacAddress;\nuse rand::Rng;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Deserialize, Serialize, Clone, Debug)]\npub struct NewInstance {\n    pub name: Option<String>,\n    pub memory_mb: Option<i32>,\n    pub cpus: Option<i32>,\n    pub host: String,\n    pub disk_size_gb: Option<i32>,\n    pub zvol_prefix: Option<String>,\n    pub distro: String,\n    pub sata: Option<bool>,\n    pub user_data: Option<String>,\n    pub join_tailnet: bool,\n}\n\npub fn random_mac() -> String {\n    let mut addr = rand::thread_rng().gen::<[u8; 6]>();\n    addr[0] = (addr[0] | 2) & 0xfe;\n    MacAddress::new(addr).to_string()\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#[macro_use]\nextern crate tracing;\n\nuse axum::{\n    routing::{delete, get, post},\n    Extension, Router,\n};\nuse axum_extra::routing::SpaRouter;\nuse std::{net::SocketAddr, sync::Arc};\nuse tower::limit::ConcurrencyLimitLayer;\nuse tower_http::trace::TraceLayer;\nuse waifud::{\n    admin,\n    api::{self, audit, cloudinit, distros, instances},\n    Config, Result, State,\n};\n\n#[tokio::main]\nasync fn main() -> Result {\n    tracing_subscriber::fmt::init();\n\n    waifud::migrate::run()?;\n\n    let cfg: Config = serde_dhall::from_file(\"./config.dhall\").parse()?;\n\n    let files = SpaRouter::new(\"/static\", \"static\");\n\n    let middleware = tower::ServiceBuilder::new()\n        .layer(TraceLayer::new_for_http())\n        .layer(ConcurrencyLimitLayer::new(64))\n        .layer(Extension(Arc::new(tailscale_client::Client::new(\n            waifud::APPLICATION_NAME.to_string(),\n            cfg.tailscale.api_key.clone(),\n            cfg.tailscale.tailnet.clone(),\n        )?)))\n        .layer(Extension(Arc::new(State::new().await?)))\n        .layer(Extension(Arc::new(cfg)));\n\n    let admin_panel = Router::new()\n        .route(\"/\", get(admin::home))\n        .route(\"/api/config\", get(admin::config))\n        .route(\"/test\", get(admin::test_handler))\n        .route(\"/instances\", get(admin::instances))\n        .route(\"/instances/create\", get(admin::instance_create))\n        .route(\"/instances/:id\", get(admin::instance))\n        .route(\"/distros\", get(admin::distro_list))\n        .layer(middleware.clone());\n\n    let cloudinit = Router::new()\n        .route(\"/:id/meta-data\", get(cloudinit::meta_data))\n        .route(\"/:id/user-data\", get(cloudinit::user_data))\n        .route(\"/:id/vendor-data\", get(cloudinit::vendor_data))\n        .layer(middleware.clone());\n\n    let api = Router::new()\n        .route(\"/auditlogs\", get(audit::list))\n        .route(\"/auditlogs/instance/:id\", get(audit::list_for_instance))\n        .route(\"/distros\", get(distros::list))\n        .route(\"/distros\", post(distros::create))\n        .route(\"/distros/:name\", post(distros::update))\n        .route(\"/distros/:name\", get(distros::get))\n        .route(\"/distros/:name\", delete(distros::delete))\n        .route(\"/instances\", post(instances::create))\n        .route(\"/instances\", get(instances::list))\n        .route(\"/instances/:id\", get(instances::get))\n        .route(\"/instances/:id/reinit\", post(instances::reinit))\n        .route(\"/instances/:id/hardreboot\", post(instances::hard_reboot))\n        .route(\"/instances/:id/reboot\", post(instances::reboot))\n        .route(\"/instances/:id/start\", post(instances::start))\n        .route(\"/instances/:id/shutdown\", post(instances::shutdown))\n        .route(\"/instances/name/:name\", get(instances::get_by_name))\n        .route(\"/instances/:id\", delete(instances::delete))\n        .route(\"/instances/:id/machine\", get(instances::get_machine))\n        .route(\"/libvirt/machines\", get(api::libvirt::get_machines))\n        .layer(middleware.clone());\n\n    let app = Router::new()\n        .nest(\"/api/v1\", api)\n        .nest(\"/api/cloudinit\", cloudinit)\n        .nest(\"/admin\", admin_panel)\n        .merge(files);\n\n    // tokio::spawn(waifud::scrape::cron());\n\n    let addr = &\"[::]:23818\".parse()?;\n    info!(\"listening on {}\", addr);\n    axum::Server::bind(addr)\n        .serve(app.into_make_service_with_connect_info::<SocketAddr>())\n        .await?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/migrate/20220225-session.sql",
    "content": "CREATE TABLE IF NOT EXISTS sessions\n  ( uuid TEXT NOT NULL PRIMARY KEY\n  , user TEXT NOT NULL\n  , expired BOOLEAN NOT NULL DEFAULT FALSE\n  );\n"
  },
  {
    "path": "src/migrate/20220814-no-session.sql",
    "content": "DROP TABLE sessions;\n"
  },
  {
    "path": "src/migrate/base_schema.sql",
    "content": "CREATE TABLE IF NOT EXISTS instances\n  ( uuid TEXT PRIMARY KEY NOT NULL\n  , name TEXT NOT NULL UNIQUE\n  , host TEXT NOT NULL\n  , mac_address TEXT NOT NULL\n  , memory INTEGER NOT NULL\n  , disk_size INTEGER NOT NULL\n  , zvol_name TEXT NOT NULL\n  , status TEXT NOT NULL DEFAULT 'unknown'\n  , distro TEXT NOT NULL\n  , join_tailnet BOOLEAN NOT NULL DEFAULT FALSE\n  );\n\nCREATE TABLE IF NOT EXISTS audit_logs\n  ( id INTEGER PRIMARY KEY AUTOINCREMENT\n  , ts INTEGER NOT NULL DEFAULT (STRFTIME('%s', 'now'))\n  , kind TEXT NOT NULL\n  , op TEXT NOT NULL\n  , data TEXT\n  , uuid TEXT GENERATED ALWAYS AS (json_extract(data, '$.uuid'))\n  , name TEXT GENERATED ALWAYS AS (json_extract(data, '$.name'))\n  );\n\nCREATE INDEX IF NOT EXISTS audit_logs_uuid\n  ON audit_logs(uuid);\n\nCREATE INDEX IF NOT EXISTS audit_logs_name\n  ON audit_logs(name);\n\nCREATE TABLE IF NOT EXISTS cloudconfig_seeds\n  ( uuid TEXT PRIMARY KEY NOT NULL\n  , user_data TEXT NOT NULL\n  );\n\nCREATE TABLE IF NOT EXISTS distros\n  ( name TEXT PRIMARY KEY NOT NULL\n  , download_url TEXT NOT NULL\n  , sha256sum TEXT NOT NULL\n  , min_size INTEGER NOT NULL\n  , format TEXT NOT NULL\n  );\n"
  },
  {
    "path": "src/migrate/mod.rs",
    "content": "use crate::establish_connection;\nuse anyhow::Result;\nuse rusqlite_migration::{Migrations, M};\n\n#[instrument(err)]\npub fn run() -> Result<()> {\n    info!(\"running\");\n    let mut conn = establish_connection()?;\n\n    let migrations = Migrations::new(vec![\n        M::up(include_str!(\"./base_schema.sql\")),\n        M::up(include_str!(\"./20220225-session.sql\")),\n        M::up(include_str!(\"./20220814-no-session.sql\")),\n    ]);\n    conn.pragma_update(None, \"journal_mode\", &\"WAL\").unwrap();\n\n    migrations.to_latest(&mut conn)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/models.rs",
    "content": "use bb8::PooledConnection;\nuse bb8_rusqlite::RusqliteConnectionManager;\nuse rusqlite::params;\nuse serde::{Deserialize, Serialize};\nuse uuid::Uuid;\n\nuse crate::Result;\n\n#[derive(Debug, Default, Deserialize, Serialize, Clone)]\npub struct Instance {\n    pub uuid: Uuid,\n    pub name: String,\n    pub host: String,\n    pub mac_address: String,\n    pub memory: i32,\n    pub disk_size: i32,\n    pub zvol_name: String,\n    pub status: String,\n    pub distro: String,\n    pub join_tailnet: bool,\n}\n\nimpl Instance {\n    pub fn from_name(\n        conn: &PooledConnection<'_, RusqliteConnectionManager>,\n        name: String,\n    ) -> Result<Self> {\n        let mut stmt = conn.prepare(\n            \"SELECT uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet FROM instances WHERE name = ?1\",\n        )?;\n        let instance = stmt.query_row(params![name], |row| {\n            Ok(Instance {\n                uuid: row.get(0)?,\n                name: row.get(1)?,\n                host: row.get(2)?,\n                mac_address: row.get(3)?,\n                memory: row.get(4)?,\n                disk_size: row.get(5)?,\n                zvol_name: row.get(6)?,\n                status: row.get(7)?,\n                distro: row.get(8)?,\n                join_tailnet: row.get(9)?,\n            })\n        })?;\n\n        Ok(instance)\n    }\n\n    pub fn from_uuid(\n        conn: &PooledConnection<'_, RusqliteConnectionManager>,\n        id: Uuid,\n    ) -> Result<Self> {\n        let mut stmt = conn.prepare(\n            \"SELECT uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet FROM instances WHERE uuid = ?1\",\n        )?;\n        let instance = stmt.query_row(params![id], |row| {\n            Ok(Instance {\n                uuid: row.get(0)?,\n                name: row.get(1)?,\n                host: row.get(2)?,\n                mac_address: row.get(3)?,\n                memory: row.get(4)?,\n                disk_size: row.get(5)?,\n                zvol_name: row.get(6)?,\n                status: row.get(7)?,\n                distro: row.get(8)?,\n                join_tailnet: row.get(9)?,\n            })\n        })?;\n\n        Ok(instance)\n    }\n}\n\npub struct CloudconfigSeed {\n    pub uuid: Uuid,\n    pub user_data: String,\n}\n\n#[derive(Deserialize, Serialize, Debug, Clone)]\npub struct Distro {\n    pub name: String,\n    #[serde(rename = \"downloadURL\")]\n    pub download_url: String,\n    #[serde(rename = \"sha256Sum\")]\n    pub sha256sum: String,\n    #[serde(rename = \"minSize\")]\n    pub min_size: i32,\n    pub format: String,\n}\n\nimpl Distro {\n    pub fn from_name(\n        conn: &PooledConnection<'_, RusqliteConnectionManager>,\n        name: String,\n    ) -> Result<Self> {\n        Ok(conn.query_row(\n            \"SELECT\n           name\n         , download_url\n         , sha256sum\n         , min_size\n         , format\n         FROM distros\n         WHERE name = ?1\",\n            params![name],\n            |row| {\n                Ok(Distro {\n                    name: row.get(0)?,\n                    download_url: row.get(1)?,\n                    sha256sum: row.get(2)?,\n                    min_size: row.get(3)?,\n                    format: row.get(4)?,\n                })\n            },\n        )?)\n    }\n}\n\n#[derive(Deserialize, Serialize, Debug, Clone)]\npub struct AuditEvent {\n    pub id: i32,\n    pub ts: i64,\n    pub kind: String,\n    pub op: String,\n    pub data: Option<serde_json::Value>,\n    pub uuid: Option<String>,\n    pub name: Option<String>,\n}\n\nimpl AuditEvent {\n    pub fn get_for_instance(\n        uuid: Uuid,\n        conn: &PooledConnection<'_, RusqliteConnectionManager>,\n    ) -> Result<Vec<AuditEvent>> {\n        let mut stmt =\n            conn.prepare(\"SELECT id, ts, kind, op, data, uuid, name from audit_logs where uuid=? and kind='instance'\")?;\n\n        let vals: Vec<AuditEvent> = stmt\n            .query_map(params![uuid.to_string()], |row| {\n                Ok(AuditEvent {\n                    id: row.get(0)?,\n                    ts: row.get(1)?,\n                    kind: row.get(2)?,\n                    op: row.get(3)?,\n                    data: row.get(4)?,\n                    uuid: row.get(5)?,\n                    name: row.get(6)?,\n                })\n            })?\n            .into_iter()\n            .map(Result::unwrap)\n            .collect();\n        Ok(vals)\n    }\n\n    pub fn get_all(\n        conn: &PooledConnection<'_, RusqliteConnectionManager>,\n    ) -> Result<Vec<AuditEvent>> {\n        let mut stmt = conn.prepare(\"SELECT id, ts, kind, op, data, uuid, name from audit_logs\")?;\n        let vals: Vec<AuditEvent> = stmt\n            .query_map(params![], |row| {\n                Ok(AuditEvent {\n                    id: row.get(0)?,\n                    ts: row.get(1)?,\n                    kind: row.get(2)?,\n                    op: row.get(3)?,\n                    data: row.get(4)?,\n                    uuid: row.get(5)?,\n                    name: row.get(6)?,\n                })\n            })?\n            .into_iter()\n            .map(Result::unwrap)\n            .collect();\n        Ok(vals)\n    }\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Session {\n    pub uuid: Uuid,\n    pub user: String,\n    pub expired: bool,\n}\n\nimpl Session {\n    #[instrument(skip(conn), err)]\n    pub fn get(\n        conn: &PooledConnection<'_, RusqliteConnectionManager>,\n        id: Uuid,\n    ) -> Result<Session> {\n        Ok(conn.query_row(\n            \"SELECT (uuid, user, expired) FROM sessions WHERE uuid = ?1\",\n            params![id],\n            |row| {\n                Ok(Session {\n                    uuid: row.get(0)?,\n                    user: row.get(1)?,\n                    expired: row.get(2)?,\n                })\n            },\n        )?)\n    }\n}\n"
  },
  {
    "path": "src/scrape/amazon_linux.rs",
    "content": "use crate::{models::Distro, Error};\nuse scraper::Html;\nuse url::Url;\n\n/// # Scraper for Amazon Linux\n///\n/// This scrapes the Amazon Linux cloud image site and extracts out the URL of the latest\n/// release of Amazon Linux and its sha256 sum.\n\npub async fn scrape() -> crate::Result<crate::models::Distro> {\n    let sel = scraper::Selector::parse(\"a\").expect(\"selector to parse\");\n\n    let res = {\n        let https = hyper_tls::HttpsConnector::new();\n        let cli = hyper::Client::builder().build::<_, hyper::Body>(https);\n        cli.get(hyper::Uri::from_static(\n            \"https://cdn.amazonlinux.com/os-images/latest/kvm/\",\n        ))\n        .await\n    }?;\n\n    let release_base = res\n        .headers()\n        .get(axum::http::header::LOCATION)\n        .ok_or(Error::Catchall(\n            \"why did the redirect not work?\".to_string(),\n        ))?\n        .to_str()?;\n\n    let response_html = reqwest::get(release_base)\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n\n    let doc = Html::parse_document(&response_html);\n\n    let link = doc\n        .select(&sel)\n        .filter(|elem| elem.value().attr(\"href\").is_some())\n        .map(|elem| elem.value().attr(\"href\").unwrap())\n        .filter(|link| link.starts_with(\"amzn2-kvm\"))\n        .take(1)\n        .map(ToString::to_string)\n        .collect::<Vec<String>>();\n    let link = link.get(0).ok_or(crate::Error::Catchall(\n        \"can't get last element of Amazon Linux image list\".to_string(),\n    ))?;\n\n    let image_url = Url::parse(release_base)?.join(link)?;\n    let shasum_url = Url::parse(release_base)?.join(\"SHA256SUMS\")?;\n\n    let shasum = reqwest::get(shasum_url)\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n\n    let shasum = shasum\n        .split(\"  \")\n        .take(1)\n        .map(ToString::to_string)\n        .collect::<Vec<String>>();\n\n    let shasum = shasum.get(0).unwrap();\n\n    Ok(Distro {\n        name: \"amazon-linux-2\".to_string(),\n        download_url: image_url.to_string(),\n        sha256sum: shasum.to_string(),\n        min_size: 25,\n        format: \"waifud://qcow2\".to_string(),\n    })\n}\n"
  },
  {
    "path": "src/scrape/arch.rs",
    "content": "use scraper::Html;\n\nuse crate::models::Distro;\n\n/// # Scraper for https://geo.mirror.pkgbuild.com/images/\n///\n/// This scrapes the Arch Linux cloud image site and extracts out the URL of the latest\n/// release of Arch Linux and its sha256 sum.\n\nconst RELEASE_BASE: &'static str = \"https://geo.mirror.pkgbuild.com/images/\";\n\npub async fn scrape() -> crate::Result<crate::models::Distro> {\n    let sel = scraper::Selector::parse(\"a\").expect(\"selector to parse\");\n\n    let response_html = reqwest::get(\"https://geo.mirror.pkgbuild.com/images/\")\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n\n    let doc = Html::parse_document(&response_html);\n\n    let link = doc.select(&sel).last().ok_or(crate::Error::Catchall(\n        \"can't get last element of Arch image list\".to_string(),\n    ))?;\n\n    let u = url::Url::parse(RELEASE_BASE)?.join(link.value().attr(\"href\").ok_or(\n        crate::Error::Catchall(\"link has no href, how???\".to_string()),\n    )?)?;\n\n    let response_html = reqwest::get(u.as_str())\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n\n    let doc = Html::parse_document(&response_html);\n\n    let links: Vec<String> = doc\n        .select(&sel)\n        .filter(|elem| elem.value().attr(\"href\").is_some())\n        .map(|elem| elem.value().attr(\"href\"))\n        .map(Option::unwrap)\n        .filter(|path| path.starts_with(\"Arch-Linux-x86_64-cloudimg-\"))\n        .map(ToString::to_string)\n        .collect();\n\n    if links.len() != 4 {\n        return Err(crate::Error::Catchall(\n            \"wrong number of things in the list, wanted 4\".to_string(),\n        ));\n    }\n\n    let image_url = links.get(0).unwrap();\n    let shasum_url = links.get(1).unwrap();\n\n    let shasum = reqwest::get(u.join(&shasum_url)?)\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n\n    let shasum = shasum\n        .split(\"  \")\n        .take(1)\n        .map(ToString::to_string)\n        .collect::<Vec<String>>();\n\n    let shasum = shasum.get(0).unwrap();\n\n    let image_url = u.join(&image_url)?.as_str().to_string();\n\n    Ok(Distro {\n        name: \"arch\".to_string(),\n        download_url: image_url,\n        sha256sum: shasum.to_string(),\n        min_size: 2,\n        format: \"waifud://qcow2\".to_string(),\n    })\n}\n"
  },
  {
    "path": "src/scrape/mod.rs",
    "content": "use std::time::Duration;\n\nuse crate::{models::Distro, Result};\nuse futures::future::join_all;\nuse rusqlite::params;\nuse tokio::time::Instant;\n\npub mod amazon_linux;\npub mod arch;\npub mod nixos;\npub mod rocky_linux;\npub mod ubuntu;\n\npub async fn get_all() -> Result<Vec<Distro>> {\n    let ubuntus = join_all(vec![\n        ubuntu::scrape((\"22.04\", \"jammy\")),\n        ubuntu::scrape((\"20.04\", \"focal\")),\n        ubuntu::scrape((\"18.04\", \"bionic\")),\n    ])\n    .await;\n    let mut result: Vec<Distro> = vec![\n        arch::scrape().await?,\n        amazon_linux::scrape().await?,\n        rocky_linux::scrape(9).await?,\n    ];\n\n    for distro in ubuntus {\n        result.push(distro?);\n    }\n\n    for distro in nixos::scrape().await? {\n        result.push(distro);\n    }\n\n    Ok(result)\n}\n\npub async fn cron() {\n    let mut conn = crate::establish_connection().unwrap();\n    'outer: loop {\n        tokio::time::sleep_until(\n            Instant::now()\n                .checked_add(Duration::from_secs(24 * 60 * 60))\n                .unwrap(),\n        )\n        .await;\n\n        debug!(\"scraping distro images from upstream\");\n\n        let distros = get_all().await.unwrap();\n\n        let tx = conn.transaction().unwrap();\n        for d in distros {\n            if let Err(why) = tx.execute(\n                \"UPDATE distros\n                 SET download_url = ?1\n                   , sha256sum    = ?2\n                   , min_size     = ?3\n                   , format       = ?4\n                 WHERE name = ?5\",\n                params![d.download_url, d.sha256sum, d.min_size, d.format, d.name],\n            ) {\n                error!(\"can't update distros: {why}\");\n                continue 'outer;\n            }\n\n            if let Err(why) = tx.execute(\n                \"INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)\",\n                params![\"distro\", \"update\", serde_json::to_string(&d).unwrap()],\n            ) {\n                error!(\"can't update audit logs: {why}\");\n                continue 'outer;\n            }\n        }\n\n        if let Err(why) = tx.commit() {\n            error!(\"can't commit transaction: {why}\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/scrape/nixos.rs",
    "content": "use crate::models::Distro;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\n#[derive(Deserialize, Serialize, Debug)]\npub struct Metadata {\n    pub fname: String,\n    pub sha256: String,\n}\n\npub async fn scrape() -> crate::Result<Vec<Distro>> {\n    let md: HashMap<String, Metadata> =\n        reqwest::get(\"https://xena.greedo.xeserv.us/pkg/nixos/metadata.json\")\n            .await?\n            .error_for_status()?\n            .json()\n            .await?;\n\n    let mut result: Vec<Distro> = vec![];\n\n    for (key, val) in md.iter() {\n        result.push(Distro {\n            name: format!(\"nixos-{key}\"),\n            download_url: format!(\"https://xena.greedo.xeserv.us/pkg/nixos/{}\", val.fname),\n            sha256sum: val.sha256.clone(),\n            min_size: 8,\n            format: \"waifud://qcow2\".to_string(),\n        })\n    }\n\n    Ok(result)\n}\n"
  },
  {
    "path": "src/scrape/rocky_linux.rs",
    "content": "use scraper::{ElementRef, Html};\n\nuse crate::models::Distro;\n\n/// # Scraper for Rocky Linux cloud images\n///\n/// This scrapes the Rocky Linux cloud image site and extracts out the URL of the latest\n/// release of Rocky Linux and its sha256 sum.\n\nconst RELEASE_BASE: &'static str = \"http://download.rockylinux.org/pub/rocky/\";\n\n#[instrument]\npub async fn scrape(version: i32) -> crate::Result<crate::models::Distro> {\n    let sel = scraper::Selector::parse(\"a\").expect(\"selector to parse\");\n\n    let mut base: String = RELEASE_BASE.to_string();\n    base.push_str(&format!(\"{}\", version));\n    base.push_str(\"/images/\");\n\n    if version == 9 {\n        base.push_str(\"x86_64/\");\n    }\n\n    let u = url::Url::parse(&base)?;\n    debug!(\"url: {u}\");\n\n    let response_html = reqwest::get(u.as_str())\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n\n    let doc = Html::parse_document(&response_html);\n\n    let elems = doc.select(&sel).collect::<Vec<ElementRef>>();\n    let elems = elems\n        .into_iter()\n        .rev()\n        .filter(|elem| elem.value().attr(\"href\").is_some())\n        .map(|elem| elem.value().attr(\"href\").unwrap())\n        .filter(|link| link.contains(\"GenericCloud\"))\n        .filter(|link| link.contains(\"x86_64\"))\n        .filter(|link| !link.contains(\"latest\"))\n        .filter(|link| link.ends_with(\".qcow2\"))\n        .collect::<Vec<&str>>();\n    let link = elems.get(0).ok_or(crate::Error::Catchall(\n        \"can't get second to last element of image list\".to_string(),\n    ))?;\n\n    let u = u.join(link)?;\n    debug!(url = u.to_string(), link = link);\n\n    let image_url = u.to_string();\n    let shasum_url = u.join(\"./CHECKSUM\")?;\n\n    debug!(\"shasum url: {shasum_url}\");\n    let mut shasums = reqwest::get(shasum_url)\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n    if version != 8 {\n        shasums = shasums\n            .split(\"\\n\")\n            .filter(|line| line.contains(\"SHA256\"))\n            .collect::<Vec<&str>>()\n            .join(\"\\n\");\n    }\n\n    let mut shasum = String::new();\n\n    for line in shasums.split(\"\\n\").filter(|line| line.contains(link)) {\n        if line == \"\" {\n            break;\n        }\n        if version != 8 {\n            let sides: Vec<&str> = line.split(\" \").collect();\n            if sides.len() != 4 {\n                error!(\"Somehow this doesn't have 3 spaces in it {line:?}\");\n                continue;\n            }\n            shasum = sides.get(3).unwrap().to_string()\n        } else {\n            let sides: Vec<&str> = line.split(\"  \").collect();\n            if sides.len() != 2 {\n                error!(\"Somehow this doesn't have two spaces in it {line:?}\");\n                continue;\n            }\n            shasum = sides.get(0).unwrap().to_string();\n        }\n    }\n\n    let image_url = u.join(&image_url)?.as_str().to_string();\n\n    Ok(Distro {\n        name: format!(\"rocky-linux-{version}\"),\n        download_url: image_url,\n        sha256sum: shasum.to_string(),\n        min_size: 10,\n        format: \"waifud://qcow2\".to_string(),\n    })\n}\n"
  },
  {
    "path": "src/scrape/ubuntu.rs",
    "content": "use scraper::{ElementRef, Html};\nuse std::collections::HashMap;\n\nuse crate::{models::Distro, Error};\n\n/// # Scraper for Ubuntu cloud images\n///\n/// This scrapes the Ubuntu cloud image site and extracts out the URL of the latest\n/// release of Ubuntu and its sha256 sum.\n\nconst RELEASE_BASE: &'static str = \"http://cloud-images.ubuntu.com/daily/server/\";\n\npub async fn scrape((version, name): (&str, &str)) -> crate::Result<crate::models::Distro> {\n    let sel = scraper::Selector::parse(\"a\").expect(\"selector to parse\");\n\n    let mut base: String = RELEASE_BASE.to_string();\n    base.push_str(&name);\n    base.push_str(\"/\");\n\n    let u = url::Url::parse(&base)?;\n    debug!(\"url: {u}\");\n\n    let response_html = reqwest::get(u.as_str())\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n\n    let doc = Html::parse_document(&response_html);\n\n    let elems = doc.select(&sel).collect::<Vec<ElementRef>>();\n    let elems = elems.into_iter().rev().collect::<Vec<ElementRef>>();\n    let link = elems.get(2).ok_or(crate::Error::Catchall(\n        \"can't get second to last element of image list\".to_string(),\n    ))?;\n\n    let u = u\n        .join(name)?\n        .join(link.value().attr(\"href\").ok_or(crate::Error::Catchall(\n            \"link has no href, how???\".to_string(),\n        ))?)?;\n    debug!(\"url: {u}\");\n\n    let response_html = reqwest::get(u.as_str())\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n\n    let doc = Html::parse_document(&response_html);\n\n    let links: Vec<String> = doc\n        .select(&sel)\n        .filter(|elem| elem.value().attr(\"href\").is_some())\n        .map(|elem| elem.value().attr(\"href\"))\n        .map(Option::unwrap)\n        .filter(|path| path.ends_with(\"-server-cloudimg-amd64.img\"))\n        .map(ToString::to_string)\n        .collect();\n\n    let image_url = links.get(0).unwrap();\n    let shasum_url = u.join(\"SHA256SUMS\")?;\n\n    let shasums = reqwest::get(shasum_url)\n        .await?\n        .error_for_status()?\n        .text()\n        .await?;\n\n    let mut sha_map = HashMap::<String, String>::new();\n\n    for line in shasums.split(\"\\n\") {\n        let sides: Vec<&str> = line.split(\" *\").collect();\n        if sides.len() != 2 {\n            error!(\"Somehow this doesn't have two spaces in it {line:?}\");\n            continue;\n        }\n\n        sha_map.insert(\n            sides.get(1).unwrap().to_string(),\n            sides.get(0).unwrap().to_string(),\n        );\n    }\n\n    // println!(\"{}\", serde_dhall::serialize(&sha_map).to_string()?);\n\n    let mut key: String = name.clone().to_string();\n    key.push_str(\"-server-cloudimg-amd64.img\");\n    let shasum = sha_map\n        .get(&key)\n        .ok_or(Error::Catchall(format!(\"can't find shasum for {name}\")))?;\n\n    let image_url = u.join(&image_url)?.as_str().to_string();\n\n    Ok(Distro {\n        name: format!(\"ubuntu-{version}\"),\n        download_url: image_url,\n        sha256sum: shasum.to_string(),\n        min_size: 5,\n        format: \"waifud://qcow2\".to_string(),\n    })\n}\n"
  },
  {
    "path": "src/tailauth.rs",
    "content": "use crate::Error;\nuse async_trait::async_trait;\nuse axum::{extract::FromRequestParts, http::request::Parts, RequestPartsExt};\n\npub struct Tailauth(pub ts_localapi::User, pub ts_localapi::WhoisPeer);\n\n#[async_trait]\nimpl<S> FromRequestParts<S> for Tailauth\nwhere\n    S: Send + Sync,\n{\n    type Rejection = Error;\n\n    async fn from_request_parts(req: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {\n        let addr: axum_client_ip::ClientIp =\n            req.extract().await.map_err(|_| Error::BadMiddlewareStack)?;\n\n        let result = ts_localapi::whois((addr.0, 0).into()).await?;\n\n        info!(\n            user = result.user_profile.login_name,\n            ip = addr.0.to_string(),\n            platform = result\n                .node\n                .clone()\n                .hostinfo\n                .os\n                .unwrap_or(\"<unknown>\".to_string())\n        );\n\n        Ok(Tailauth(result.user_profile, result.node))\n    }\n}\n"
  },
  {
    "path": "templates/base.rs.xml",
    "content": "@(name: String, uuid: String, mac_address: String, zvol: String, sata: bool, memory: i32, cpus: i32, seed: String, qemu_path: String)\n<domain type=\"kvm\" xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>\n  <name>@name</name>\n  <uuid>@uuid</uuid>\n  <memory>@memory</memory>\n  <currentMemory>@memory</currentMemory>\n  <vcpu>@cpus</vcpu>\n  <os>\n    <type arch=\"x86_64\" machine=\"pc-q35-8.0\">hvm</type>\n    <loader readonly=\"yes\" type=\"pflash\">/run/libvirt/nix-ovmf/OVMF_CODE.fd</loader>\n    <boot dev=\"hd\"/>\n  </os>\n  <features>\n    <acpi/>\n    <apic/>\n    <vmport state=\"off\"/>\n  </features>\n  <cpu mode=\"host-model\"/>\n  <clock offset=\"utc\">\n    <timer name=\"rtc\" tickpolicy=\"catchup\"/>\n    <timer name=\"pit\" tickpolicy=\"delay\"/>\n    <timer name=\"hpet\" present=\"no\"/>\n  </clock>\n  <on_poweroff>destroy</on_poweroff>\n  <on_reboot>restart</on_reboot>\n  <on_crash>destroy</on_crash>\n  <pm>\n    <suspend-to-mem enabled=\"no\"/>\n    <suspend-to-disk enabled=\"no\"/>\n  </pm>\n  <devices>\n    <emulator>@qemu_path</emulator>\n    <disk type=\"block\" device=\"disk\">\n      <driver name=\"qemu\" type=\"raw\" cache=\"none\" io=\"native\"/>\n      <source dev=\"/dev/zvol/@zvol/@name\"/>\n      @if sata {\n      <target dev=\"sda\" bus=\"sata\"/>\n      } else {\n      <target dev=\"vda\" bus=\"virtio\"/>\n      }\n    </disk>\n    <controller type=\"usb\" model=\"qemu-xhci\" ports=\"15\"/>\n    <interface type=\"network\">\n      <source network=\"default\"/>\n      <mac address=\"@mac_address\"/>\n      @if sata {\n      <model type=\"e1000e\"/>\n      <address type=\"pci\" domain=\"0x0000\" bus=\"0x01\" slot=\"0x00\" function=\"0x0\"/>\n      } else {\n      <model type=\"virtio\"/>\n      }\n    </interface>\n    <console type=\"pty\"/>\n    <channel type=\"unix\">\n      <source mode=\"bind\"/>\n      <target type=\"virtio\" name=\"org.qemu.guest_agent.0\"/>\n    </channel>\n    <channel type=\"spicevmc\">\n      <target type=\"virtio\" name=\"com.redhat.spice.0\"/>\n    </channel>\n    <input type=\"tablet\" bus=\"usb\"/>\n    <graphics type=\"spice\" port=\"-1\" tlsPort=\"-1\" autoport=\"yes\"/>\n    <sound model=\"ich9\"/>\n    <video>\n      <model type=\"qxl\"/>\n    </video>\n    <redirdev bus=\"usb\" type=\"spicevmc\"/>\n    <redirdev bus=\"usb\" type=\"spicevmc\"/>\n    <memballoon model=\"virtio\"/>\n    <rng model=\"virtio\">\n      <backend model=\"random\">/dev/urandom</backend>\n    </rng>\n  </devices>\n  <qemu:commandline>\n    <qemu:arg value=\"-smbios\" />\n    <qemu:arg value=\"type=1,sku=waifud-@(memory)m-@(cpus)c,serial=ds=nocloud-net;s=@seed\" />\n  </qemu:commandline>\n</domain>\n"
  },
  {
    "path": "templates/base.xml",
    "content": "<domain type=\"kvm\" xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>\n  <name>{{.Name}}</name>\n  <uuid>{{.UUID}}</uuid>\n  <metadata>\n    <libosinfo:libosinfo xmlns:libosinfo=\"http://libosinfo.org/xmlns/libvirt/domain/1.0\">\n      <libosinfo:os id=\"http://nixos.org/nixos/unstable\"/>\n    </libosinfo:libosinfo>\n  </metadata>\n  <memory>{{.Memory}}</memory>\n  <currentMemory>{{.Memory}}</currentMemory>\n  <vcpu>2</vcpu>\n  <os>\n    <type arch=\"x86_64\" machine=\"q35\">hvm</type>\n    <boot dev=\"hd\"/>\n  </os>\n  <features>\n    <acpi/>\n    <apic/>\n    <vmport state=\"off\"/>\n  </features>\n  <cpu mode=\"host-model\"/>\n  <clock offset=\"utc\">\n    <timer name=\"rtc\" tickpolicy=\"catchup\"/>\n    <timer name=\"pit\" tickpolicy=\"delay\"/>\n    <timer name=\"hpet\" present=\"no\"/>\n  </clock>\n  <on_poweroff>destroy</on_poweroff>\n  <on_reboot>restart</on_reboot>\n  <on_crash>destroy</on_crash>\n  <pm>\n    <suspend-to-mem enabled=\"no\"/>\n    <suspend-to-disk enabled=\"no\"/>\n  </pm>\n  <devices>\n    <emulator>/run/libvirt/nix-emulators/qemu-system-x86_64</emulator>\n    <disk type=\"block\" device=\"disk\">\n      <driver name=\"qemu\" type=\"raw\" cache=\"none\" io=\"native\"/>\n      <source dev=\"/dev/zvol/{{.ZVol}}\"/>\n      {{if .SATA}}\n      <target dev=\"sda\" bus=\"sata\"/>\n      {{else}}\n      <target dev=\"vda\" bus=\"virtio\"/>\n      {{end}}\n    </disk>\n    <disk type=\"file\" device=\"cdrom\">\n      <driver name=\"qemu\" type=\"raw\"/>\n      <source file=\"{{.Seed}}\"/>\n      <target dev=\"sdb\" bus=\"sata\"/>\n      <readonly/>\n    </disk>\n    <controller type=\"usb\" model=\"qemu-xhci\" ports=\"15\"/>\n    <interface type=\"network\">\n      <source network=\"default\"/>\n      <mac address=\"{{.MACAddress}}\"/>\n      {{if .SATA}}\n      <model type=\"e1000e\"/>\n      <address type=\"pci\" domain=\"0x0000\" bus=\"0x01\" slot=\"0x00\" function=\"0x0\"/>\n      {{else}}\n      <model type=\"virtio\"/>\n      {{end}}\n    </interface>\n    <console type=\"pty\"/>\n    <channel type=\"unix\">\n      <source mode=\"bind\"/>\n      <target type=\"virtio\" name=\"org.qemu.guest_agent.0\"/>\n    </channel>\n    <channel type=\"spicevmc\">\n      <target type=\"virtio\" name=\"com.redhat.spice.0\"/>\n    </channel>\n    <input type=\"tablet\" bus=\"usb\"/>\n    <graphics type=\"spice\" port=\"-1\" tlsPort=\"-1\" autoport=\"yes\"/>\n    <sound model=\"ich9\"/>\n    <video>\n      <model type=\"qxl\"/>\n    </video>\n    <redirdev bus=\"usb\" type=\"spicevmc\"/>\n    <redirdev bus=\"usb\" type=\"spicevmc\"/>\n    <memballoon model=\"virtio\"/>\n    <rng model=\"virtio\">\n      <backend model=\"random\">/dev/urandom</backend>\n    </rng>\n  </devices>\n  <qemu:commandline>\n    <qemu:arg value=\"-smbios\" />\n    <qemu:arg value=\"type=1,serial=ds=nocloud;h={{.Name}}\" />\n  </qemu:commandline>\n</domain>\n"
  },
  {
    "path": "templates/meta-data",
    "content": "instance-id: {{.ID}}\nlocal-hostname: {{.Name}}\n"
  },
  {
    "path": "templates/templates.go",
    "content": "package templates\n\nimport \"embed\"\n\n//go:embed meta-data *.xml\nvar FS embed.FS\n"
  },
  {
    "path": "var/.gitignore",
    "content": "*.db*\nfiles\n*.pubkey\n*.privkey\n"
  },
  {
    "path": "var/base.yaml",
    "content": "#cloud-config\n#vim:syntax=yaml\n\n# See https://cloudinit.readthedocs.io/en/latest/topics/examples.html\n# for examples of what to put in here.\n\nusers:\n  - name: user\n    groups: [ wheel ]\n    sudo: [ \"ALL=(ALL) NOPASSWD:ALL\" ]\n    shell: /bin/bash\n    ssh-authorized-keys:\n      - # get your authorized key from `ssh-add -L`\n"
  },
  {
    "path": "var/xe-base-windows.yaml",
    "content": "#cloud-config\n#vim:syntax=yaml\n\nusers:\n  - name: Xe\n    passwd: \"hunter2\"\n    primary_group: Administrators\n    groups: [ Administrators, Users ]\n    ssh_authorized_keys:\n      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPg9gYKVglnO2HQodSJt4z4mNrUSUiyJQ7b+J798bwD9 cadey@shachi\n"
  },
  {
    "path": "var/xe-base.nix",
    "content": "{ config, pkgs, modulesPath, ... }:\n\n{\n  imports = [ (modulesPath + \"/profiles/qemu-guest.nix\") ];\n\n  boot.initrd.availableKernelModules =\n    [ \"ata_piix\" \"uhci_hcd\" \"virtio_pci\" \"sr_mod\" \"virtio_blk\" ];\n  boot.initrd.kernelModules = [ ];\n  boot.kernelModules = [ ];\n  boot.extraModulePackages = [ ];\n\n  users.users.xe = {\n    isNormalUser = true;\n    initialPassword = \"hunter2\";\n    extraGroups = [ \"wheel\" ];\n    openssh.authorizedKeys.keys = [\n      \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPg9gYKVglnO2HQodSJt4z4mNrUSUiyJQ7b+J798bwD9\"\n    ];\n  };\n\n  services.openssh.enable = true;\n\n  security.sudo.wheelNeedsPassword = false;\n  services.cloud-init = {\n    enable = true;\n    ext4.enable = true;\n  };\n}\n"
  },
  {
    "path": "var/xe-base.yaml",
    "content": "#cloud-config\n#vim:syntax=yaml\n\nusers:\n  - name: root\n    groups: [ wheel ]\n    sudo: [ \"ALL=(ALL) NOPASSWD:ALL\" ]\n    shell: /bin/sh\n    ssh-authorized-keys:\n      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPg9gYKVglnO2HQodSJt4z4mNrUSUiyJQ7b+J798bwD9\n      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPYr9hiLtDHgd6lZDgQMkJzvYeAXmePOrgFaWHAjJvNU\n"
  }
]