diff options
author | Aspen Smith <grfn@gws.fyi> | 2024-02-12T03·00-0500 |
---|---|---|
committer | clbot <clbot@tvl.fyi> | 2024-02-14T19·37+0000 |
commit | 82ecd61f5c699cf3af6c4eadf47a1c52b1d696c6 (patch) | |
tree | 429c5e078528000591742ec3211bc768ae913a78 /users/aspen/bbbg | |
parent | 0ba476a4266015f278f18d74094299de74a5a111 (diff) |
chore(users): grfn -> aspen r/7511
Change-Id: I6c6847fac56f0a9a1a2209792e00a3aec5e672b9 Reviewed-on: https://cl.tvl.fyi/c/depot/+/10809 Autosubmit: aspen <root@gws.fyi> Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI Reviewed-by: lukegb <lukegb@tvl.fyi>
Diffstat (limited to 'users/aspen/bbbg')
75 files changed, 5172 insertions, 0 deletions
diff --git a/users/aspen/bbbg/.clj-kondo/config.edn b/users/aspen/bbbg/.clj-kondo/config.edn new file mode 100644 index 000000000000..8faddb77ecc4 --- /dev/null +++ b/users/aspen/bbbg/.clj-kondo/config.edn @@ -0,0 +1 @@ +{:lint-as {garden.def/defstyles clojure.core/def}} diff --git a/users/aspen/bbbg/.envrc b/users/aspen/bbbg/.envrc new file mode 100644 index 000000000000..051d09d292a8 --- /dev/null +++ b/users/aspen/bbbg/.envrc @@ -0,0 +1 @@ +eval "$(lorri direnv)" diff --git a/users/aspen/bbbg/.gitignore b/users/aspen/bbbg/.gitignore new file mode 100644 index 000000000000..99dbfc443636 --- /dev/null +++ b/users/aspen/bbbg/.gitignore @@ -0,0 +1,9 @@ +/target +/classes +*.jar +*.class +/.nrepl-port +/.cpcache +/.clojure +/result +/.clj-kondo/.cache diff --git a/users/aspen/bbbg/Makefile b/users/aspen/bbbg/Makefile new file mode 100644 index 000000000000..fc45477984e3 --- /dev/null +++ b/users/aspen/bbbg/Makefile @@ -0,0 +1,2 @@ +deps.nix: deps.edn + clj2nix ./deps.edn ./deps.nix '-A:uberjar' '-A:clj-test' diff --git a/users/aspen/bbbg/README.md b/users/aspen/bbbg/README.md new file mode 100644 index 000000000000..41f59319cb99 --- /dev/null +++ b/users/aspen/bbbg/README.md @@ -0,0 +1,129 @@ +# Brooklyn-Based Board Gaming signup sheet + +This directory contains a small web application that acts as a signup +sheet and attendee tracking system for [my local board gaming +meetup](https://www.meetup.com/brooklyn-based-board-gaming/). + +## Development + +### Installing dependencies + +#### With Nix + Docker ("blessed way") + +Prerequisites: + +- [Nix](https://nixos.org/) +- [lorri](https://github.com/nix-community/lorri) +- [Docker](https://www.docker.com/) + +From this directory in a full checkout of depot, run the following +commands to install all development dependencies: + +``` shell-session +$ pwd +/path/to/depot/users/aspen/bbbg +$ direnv allow +$ lorri watch --once # Wait for a single nix shell build +``` + +Then, to run a docker container with the development database: + +``` shell-session +$ pwd +/path/to/depot/users/aspen/bbbg +$ arion up -d +``` + +#### Choose-your-own-adventure + +Note that the **authoritative** source for dev dependencies is the `shell.nix` +file in this directory - those may diverge from what's written here; if so +follow those versions rather than these. + +- Install the [clojure command-line + tools](https://clojure.org/guides/getting_started), with openjdk 11 +- Install and run a postgresql 12 database, with: + - A user with superuser priveleges, the username `bbbg` and the + password `password` + - A database called `bbbg` owned by that user. +- Export the following environment variables in a context visible by + whatever method you use to run the application: + - `PGHOST=localhost` + - `PGUSER=bbbg` + - `PGDATABASE=bbbg` + - `PGPASSWORD=bbbg` + +### Running the application + +Before running the app, you'll need an oauth2 client-id and client secret for a +Discord app. The application can either load those from a +[pass](https://www.passwordstore.org/) password store, or read them from +plaintext files in a directory. In either case, they should be accessible at the +paths `bbbg/discord-client-id` and `bbbg/discord-client-secret` respectively. + +#### From the command line + +``` shell-session +$ clj -A:dev +Clojure 1.11.0-alpha3 +user=> (require 'bbbg.core) +nil +user=> ;; Optionally, if you're using a directory with plaintext files for the discord client ID and client secret: +user=> (bbbg.util.dev-secrets/set-backend! [:dir "/path/to/that/directory"]) +user=> (bbbg.core/run-dev) +##<SystemMap> +user=> (bbbg.db/migrate! (:db bbbg.core/system)) +11:57:26.536 [main] INFO migratus.core - Starting migrations { } +11:57:26.538 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... { } +11:57:26.883 [main] INFO com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.impossibl.postgres.jdbc.PGDirectConnection@3cae770e { } +11:57:26.884 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. { } +11:57:26.923 [main] INFO migratus.core - Ending migrations { } +nil +``` + +This will run a web server for the application listening at +<http://localhost:8888> + +#### In Emacs, with [CIDER](https://docs.cider.mx/cider/index.html) + [direnv](https://github.com/wbolster/emacs-direnv) + +Open `//users/aspen/bbbg/src/bbbg/core.clj` in a buffer, then follow the +instructions at the end of the file + +## Deployment + +### With nix+terraform + +Deployment configuration is located in the `tf.nix` file, which is +currently tightly coupled to my own infrastructure and AWS account but +could hypothetically be adjusted to be general-purpose. + +To deploy a new version of the application, after following "installing +dependencies" above, run the following command in a context with ec2 +credentials available: + +``` shell-session +$ terraform apply +``` + +The current deploy configuration includes: + +- An ec2 instance running nixos, with a postgresql database and the + bbbg application running as a service, behind nginx with an + auto-renewing letsencrypt cert +- The DNS A record for `bbbg.gws.fyi` pointing at that ec2 instance, + in the cloudflare zone for `gws.fyi` + +### Otherwise + +¯\\\_(ツ)_/¯ + +You'll need: + +- An uberjar for bbbg; the canonical way of building that is `nix-build + /path/to/depot -A users.aspen.bbbg.server-jar` but I\'m not sure how that + works outside of nix +- A postgresql database +- Environment variables telling the app how to connect to that + database. See `config.systemd.services.bbbg-server.environment` in + `module.nix` for which env vars are currently being exported by the + NixOS module that runs the production version of the app diff --git a/users/aspen/bbbg/arion-compose.nix b/users/aspen/bbbg/arion-compose.nix new file mode 100644 index 000000000000..c8a6dd156d2c --- /dev/null +++ b/users/aspen/bbbg/arion-compose.nix @@ -0,0 +1,15 @@ +{ ... }: + +{ + services = { + postgres.service = { + image = "postgres:12"; + environment = { + POSTGRES_DB = "bbbg"; + POSTGRES_USER = "bbbg"; + POSTGRES_PASSWORD = "password"; + }; + ports = [ "5432:5432" ]; + }; + }; +} diff --git a/users/aspen/bbbg/arion-pkgs.nix b/users/aspen/bbbg/arion-pkgs.nix new file mode 100644 index 000000000000..c6d603be2a99 --- /dev/null +++ b/users/aspen/bbbg/arion-pkgs.nix @@ -0,0 +1,2 @@ +let depot = import ../../.. { }; +in depot.third_party.nixpkgs diff --git a/users/aspen/bbbg/default.nix b/users/aspen/bbbg/default.nix new file mode 100644 index 000000000000..6afb68353c44 --- /dev/null +++ b/users/aspen/bbbg/default.nix @@ -0,0 +1,82 @@ +args@{ depot, pkgs, ... }: + +with pkgs.lib; + +let + inherit (depot.third_party) gitignoreSource; + + deps = import ./deps.nix { + inherit (pkgs) fetchMavenArtifact fetchgit lib; + }; +in +rec { + meta.ci.targets = [ + "db-util" + "server" + "tf" + ]; + + depsPaths = deps.makePaths { }; + + resources = builtins.filterSource (_: type: type != "symlink") ./resources; + + classpath.dev = concatStringsSep ":" ( + (map gitignoreSource [ ./src ./test ./env/dev ]) ++ [ resources ] ++ depsPaths + ); + + classpath.test = concatStringsSep ":" ( + (map gitignoreSource [ ./src ./test ./env/test ]) ++ [ resources ] ++ depsPaths + ); + + classpath.prod = concatStringsSep ":" ( + (map gitignoreSource [ ./src ./env/prod ]) ++ [ resources ] ++ depsPaths + ); + + testClojure = pkgs.writeShellScript "test-clojure" '' + export HOME=$(pwd) + ${pkgs.clojure}/bin/clojure -Scp ${depsPaths} + ''; + + mkJar = name: opts: + with pkgs; + assert (hasSuffix ".jar" name); + stdenv.mkDerivation rec { + inherit name; + dontUnpack = true; + buildPhase = '' + export HOME=$(pwd) + cp ${./pom.xml} pom.xml + cp ${./deps.edn} deps.edn + ${clojure}/bin/clojure \ + -Scp ${classpath.prod} \ + -A:uberjar \ + ${name} \ + -C ${opts} + ''; + + doCheck = true; + + checkPhase = '' + echo "checking for existence of ${name}" + [ -f ${name} ] + ''; + + installPhase = '' + cp ${name} $out + ''; + }; + + db-util-jar = mkJar "bbbg-db-util.jar" "-m bbbg.db"; + + db-util = pkgs.writeShellScriptBin "bbbg-db-util" '' + exec ${pkgs.openjdk17_headless}/bin/java -jar ${db-util-jar} "$@" + ''; + + server-jar = mkJar "bbbg-server.jar" "-m bbbg.core"; + + server = pkgs.writeShellScriptBin "bbbg-server" '' + exec ${pkgs.openjdk17_headless}/bin/java -jar ${server-jar} "$@" + ''; + + tf = import ./tf.nix args; +} diff --git a/users/aspen/bbbg/deps.edn b/users/aspen/bbbg/deps.edn new file mode 100644 index 000000000000..39ce843c22b5 --- /dev/null +++ b/users/aspen/bbbg/deps.edn @@ -0,0 +1,70 @@ +{:deps + {org.clojure/clojure {:mvn/version "1.11.0-alpha3"} + + ;; DB + com.github.seancorfield/next.jdbc {:mvn/version "1.2.761"} + com.impossibl.pgjdbc-ng/pgjdbc-ng {:mvn/version "0.8.9"} + com.zaxxer/HikariCP {:mvn/version "5.0.0"} + migratus/migratus {:mvn/version "1.3.5"} + com.github.seancorfield/honeysql {:mvn/version "2.2.840"} + nilenso/honeysql-postgres {:mvn/version "0.4.112"} + + ;; HTTP + http-kit/http-kit {:mvn/version "2.5.3"} + ring/ring {:mvn/version "1.9.4"} + compojure/compojure {:mvn/version "1.6.2"} + javax.servlet/servlet-api {:mvn/version "2.5"} + ring-oauth2/ring-oauth2 {:mvn/version "0.2.0"} + clj-http/clj-http {:mvn/version "3.12.3"} + ring-logger/ring-logger {:mvn/version "1.0.1"} + + ;; Web + hiccup/hiccup {:mvn/version "1.0.5"} + garden/garden {:mvn/version "1.3.10"} + + + ;; Logging + Observability + ch.qos.logback/logback-classic {:mvn/version "1.3.0-alpha12"} + org.slf4j/jul-to-slf4j {:mvn/version "2.0.0-alpha4"} + org.slf4j/jcl-over-slf4j {:mvn/version "2.0.0-alpha4"} + org.slf4j/log4j-over-slf4j {:mvn/version "2.0.0-alpha4"} + cambium/cambium.core {:mvn/version "1.1.1"} + cambium/cambium.codec-cheshire {:mvn/version "1.0.0"} + cambium/cambium.logback.core {:mvn/version "0.4.5"} + cambium/cambium.logback.json {:mvn/version "0.4.5"} + clj-commons/iapetos {:mvn/version "0.1.12"} + + ;; Utilities + com.stuartsierra/component {:mvn/version "1.0.0"} + yogthos/config {:mvn/version "1.1.9"} + clojure.java-time/clojure.java-time {:mvn/version "0.3.3"} + cheshire/cheshire {:mvn/version "5.10.1"} + org.apache.commons/commons-lang3 {:mvn/version "3.12.0"} + org.clojure/data.csv {:mvn/version "1.0.0"} + + ;; Spec + org.clojure/spec.alpha {:mvn/version "0.3.218"} + org.clojure/core.specs.alpha {:mvn/version "0.2.62"} + expound/expound {:mvn/version "0.8.10"} + org.clojure/test.check {:mvn/version "1.1.1"}} + + :paths + ["src" + "test" + "resources" + "target/classes"] + :aliases + {:dev {:extra-paths ["env/dev"] + :jvm-opts ["-XX:-OmitStackTraceInFastThrow"]} + :clj-test {:extra-paths ["test" "env/test"] + :extra-deps {io.github.cognitect-labs/test-runner + {:git/url "https://github.com/cognitect-labs/test-runner" + :sha "cc75980b43011773162b485f46f939dc5fba91e4"}} + :main-opts ["-m" "cognitect.test-runner" + "-d" "test"]} + :uberjar {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}} + :extra-paths ["env/prod"] + :main-opts ["-m" "hf.depstar.uberjar"]} + + :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "1.3.1"}} + :main-opts ["-m" "antq.core"]}}} diff --git a/users/aspen/bbbg/deps.nix b/users/aspen/bbbg/deps.nix new file mode 100644 index 000000000000..02f5ecb4683c --- /dev/null +++ b/users/aspen/bbbg/deps.nix @@ -0,0 +1,1494 @@ +# generated by clj2nix-1.1.0-rc +{ fetchMavenArtifact, fetchgit, lib }: + +let + repos = [ + "https://repo1.maven.org/maven2/" + "https://repo.clojars.org/" + ]; + +in +rec { + makePaths = { extraClasspaths ? [ ] }: + if (builtins.typeOf extraClasspaths != "list") + then builtins.throw "extraClasspaths must be of type 'list'!" + else (lib.concatMap + (dep: + builtins.map + (path: + if builtins.isString path then + path + else if builtins.hasAttr "jar" path then + path.jar + else if builtins.hasAttr "outPath" path then + path.outPath + else + path + ) + dep.paths) + packages) ++ extraClasspaths; + makeClasspaths = { extraClasspaths ? [ ] }: + if (builtins.typeOf extraClasspaths != "list") + then builtins.throw "extraClasspaths must be of type 'list'!" + else builtins.concatStringsSep ":" (makePaths { inherit extraClasspaths; }); + packageSources = builtins.map (dep: dep.src) packages; + packages = [ + rec { + name = "cambium.logback.json/cambium"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "cambium.logback.json"; + groupId = "cambium"; + sha512 = "8e3f32bc1e11071ddc8700204333ba653585de7985c03d14c351950a7896975092e9deffd658bfec7b0b8b9cc72dc025d8e5179a185bd25da26e500218ec37a5"; + version = "0.4.5"; + + }; + paths = [ src ]; + } + + rec { + name = "clojure/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clojure"; + groupId = "org.clojure"; + sha512 = "a242514f623a17601b360886563c4a4fe09335e4e16522ac42bbcacda073ae77651cfed446daae7fe74061bb7dff5adc454769c0edc0ded350136c3c707e75b9"; + version = "1.11.0-alpha3"; + + }; + paths = [ src ]; + } + + rec { + name = "joda-time/joda-time"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "joda-time"; + groupId = "joda-time"; + sha512 = "012fb9aa9b00b456f72a92374855a7f062f8617c026c436eee2cda67dffa2f8622201909c0f4f454bb346ff5a3ed6f60c236fafb19fa66f612d9861f27b38d3a"; + version = "2.10"; + + }; + paths = [ src ]; + } + + rec { + name = "commons-codec/commons-codec"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-codec"; + groupId = "commons-codec"; + sha512 = "da30a716770795fce390e4dd340a8b728f220c6572383ffef55bd5839655d5611fcc06128b2144f6cdcb36f53072a12ec80b04afee787665e7ad0b6e888a6787"; + version = "1.15"; + + }; + paths = [ src ]; + } + + rec { + name = "HikariCP/com.zaxxer"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "HikariCP"; + groupId = "com.zaxxer"; + sha512 = "a41b6d8b1c4656e633459824f10320965976eeead01bd5cb24911040073181730e61feb797aef89d9e01c922e89cb58654f364df0a6b1bf62ab3e6f9cc367d77"; + version = "5.0.0"; + + }; + paths = [ src ]; + } + + rec { + name = "ring-devel/ring"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ring-devel"; + groupId = "ring"; + sha512 = "79a1ec9f9d03aa4fa0426353970b13468ee65ce314b51ab7a2682212a196a9b5c985eacdee5dbc6ff2f1b536a4e06d0e85e9dd7cc9a49958735c9c4e6d427fd5"; + version = "1.9.4"; + + }; + paths = [ src ]; + } + + rec { + name = "simpleclient/io.prometheus"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "simpleclient"; + groupId = "io.prometheus"; + sha512 = "60af1cefff04e7036467eae54f5930d5677e4ab066f8ed38a391b54df17733acfefac45e19ee53cef289347bddce5fc69a2766f4e580d21a22cfd9e2348e2723"; + version = "0.12.0"; + + }; + paths = [ src ]; + } + + rec { + name = "commons-lang3/org.apache.commons"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-lang3"; + groupId = "org.apache.commons"; + sha512 = "fbdbc0943cb3498b0148e86a39b773f97c8e6013740f72dbc727faeabea402073e2cc8c4d68198e5fc6b08a13b7700236292e99d4785f2c9989f2e5fac11fd81"; + version = "3.12.0"; + + }; + paths = [ src ]; + } + + rec { + name = "tools.logging/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "tools.logging"; + groupId = "org.clojure"; + sha512 = "b7a9680f1156fc7c1574a4364ca550d47668ba727fc80110fdd00c159bedb45c5be82f09cdfb8e8e988e3381e2cf8881ea70651e38001e3eaa4ece31ad0bf0c5"; + version = "1.2.2"; + + }; + paths = [ src ]; + } + + rec { + name = "core.specs.alpha/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "core.specs.alpha"; + groupId = "org.clojure"; + sha512 = "f521f95b362a47bb35f7c85528c34537f905fb3dd24f2284201e445635a0df701b35d8419d53c6507cc78d3717c1f83cda35ea4c82abd8943cd2ab3de3fcad70"; + version = "0.2.62"; + + }; + paths = [ src ]; + } + + rec { + name = "netty-common/io.netty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "netty-common"; + groupId = "io.netty"; + sha512 = "7efc2f6774a3dbe8408fe182e19830b5b7a994a0d1b0eb50699df691c2450befa05ac205bbf341ad57bef3a04281ce435031e97e725c5c4edfc705a418828ce8"; + version = "4.1.63.Final"; + + }; + paths = [ src ]; + } + + rec { + name = "jackson-databind/com.fasterxml.jackson.core"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jackson-databind"; + groupId = "com.fasterxml.jackson.core"; + sha512 = "9f771e78af669b1e1683d6c5903bbf4790aaa88b6b420c2018437da318c3fa4220cd7fa726f3e42a1b8075def1fdbd3744937c15f3bcedfca3050199247363e8"; + version = "2.12.4"; + + }; + paths = [ src ]; + } + + rec { + name = "expound/expound"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "expound"; + groupId = "expound"; + sha512 = "ca0a57cfd215cff6be36d1f83461ec2d0559c0eae172c8a8bd6e1676d49933d3c30a71192889bd75d813581707d5eda0ec05de03326396bc0cedebf2d71811e5"; + version = "0.8.10"; + + }; + paths = [ src ]; + } + + rec { + name = "spec.alpha/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "spec.alpha"; + groupId = "org.clojure"; + sha512 = "ddfe4fa84622abd8ac56e2aa565a56e6bdc0bf330f377ff3e269ddc241bb9dbcac332c13502dfd4c09c2c08fe24d8d2e8cf3d04a1bc819ca5657b4e41feaa7c2"; + version = "0.3.218"; + + }; + paths = [ src ]; + } + + rec { + name = "tools.cli/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "tools.cli"; + groupId = "org.clojure"; + sha512 = "1d88aa03eb6a664bf2c0ce22c45e7296d54d716e29b11904115be80ea1661623cf3e81fc222d164047058239010eb678af92ffedc7c3006475cceb59f3b21265"; + version = "1.0.206"; + + }; + paths = [ src ]; + } + + rec { + name = "compojure/compojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "compojure"; + groupId = "compojure"; + sha512 = "1f4ba1354bd95772963a4ef0e129dde59d16f4f9fac0f89f2505a1d5de3b4527e45073219c0478e0b3285da46793e7c145ec5a55a9dae2fca6b77dc8d67b4db6"; + version = "1.6.2"; + + }; + paths = [ src ]; + } + + rec { + name = "commons-fileupload/commons-fileupload"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-fileupload"; + groupId = "commons-fileupload"; + sha512 = "a8780b7dd7ab68f9e1df38e77a5207c45ff50ec53d8b1476570d069edc8f59e52fb1d0fc534d7e513ac5a01b385ba73c320794c82369a72bd6d817a3b3b21f39"; + version = "1.4"; + + }; + paths = [ src ]; + } + + rec { + name = "jetty-http/org.eclipse.jetty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jetty-http"; + groupId = "org.eclipse.jetty"; + sha512 = "60422ff3ef311f1d9d7340c2accdf611d40e738a39e9128967175ede4990439f4725995988849957742d488f749dd2e0740f74dc5bd9b3364e32fbaa66689308"; + version = "9.4.42.v20210604"; + + }; + paths = [ src ]; + } + + rec { + name = "jetty-util/org.eclipse.jetty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jetty-util"; + groupId = "org.eclipse.jetty"; + sha512 = "d69084e2cfe0c3af1dc7ee2745d563549a4068b6e8aed5cd2b9f31167168fb64d418c4134a6dfb811b627ec0051d7ff71e0a02e4e775d18a53543d0871c44730"; + version = "9.4.42.v20210604"; + + }; + paths = [ src ]; + } + + rec { + name = "janino/org.codehaus.janino"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "janino"; + groupId = "org.codehaus.janino"; + sha512 = "6853d7d53d3629df43a3a17ff5c989f59ec14e9030be5f67426deb9d0797fa3996b0609d582c65f22a4f7680c941b39ab6d466c480b2fea4bf92218a9b89651d"; + version = "3.1.2"; + + }; + paths = [ src ]; + } + + rec { + name = "jcl-over-slf4j/org.slf4j"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jcl-over-slf4j"; + groupId = "org.slf4j"; + sha512 = "23662fe407fcdbcba8865a8cd3f8bb09d4eb178a2a6511a32e35b995722b345e73f5dc1dd85d2d0a5c707db05aa57e0b3d0b96b59e55403fc486343d5ca4c0d6"; + version = "2.0.0-alpha4"; + + }; + paths = [ src ]; + } + + (rec { + name = "io.github.cognitect-labs/test-runner"; + src = fetchgit { + name = "test-runner"; + url = "https://github.com/cognitect-labs/test-runner"; + rev = "cc75980b43011773162b485f46f939dc5fba91e4"; + sha256 = "1661ddmmqva1yiz9p09i5l32lfpi0a99h56022zgvz03nca2ksbg"; + }; + paths = map (path: src + path) [ + "/src" + ]; + }) + + rec { + name = "cambium.logback.core/cambium"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "cambium.logback.core"; + groupId = "cambium"; + sha512 = "83ee9a583dd8a7b2e82e0981b4e51b005095a27257eb1b07165d9701645609060c466ae67fb9431f524a544d52b71fa00009b8acf05aadbeb549043515f9b382"; + version = "0.4.5"; + + }; + paths = [ src ]; + } + + rec { + name = "httpasyncclient/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpasyncclient"; + groupId = "org.apache.httpcomponents"; + sha512 = "0a80db5dbf772f02d02ba6c7c163e8da9517dd7195714b495acb845c429580c1fc926d3e71c115e75be8c145651dce2fdfa0dc380132f7809c14b3ad95492aee"; + version = "4.1.4"; + + }; + paths = [ src ]; + } + + rec { + name = "logback-jackson/ch.qos.logback.contrib"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "logback-jackson"; + groupId = "ch.qos.logback.contrib"; + sha512 = "d9a3d4cb6cf4eda6fc18e2d374007d27c6ddba98e989a8d8a01b49859b280450113f685df6e16c5fbe0472bc9e26308bc7e8b7e0aedab9404cf0b492d7511685"; + version = "0.1.5"; + + }; + paths = [ src ]; + } + + rec { + name = "simpleclient_tracer_otel/io.prometheus"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "simpleclient_tracer_otel"; + groupId = "io.prometheus"; + sha512 = "bce192e6162cb3ada7dd6c2d10456e78bce71c170faa09bad2896272fa1bd4a036288d707f3d47747991d8946c74fe21c565713fb15c7052305eb753c94dd939"; + version = "0.12.0"; + + }; + paths = [ src ]; + } + + rec { + name = "netty-codec/io.netty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "netty-codec"; + groupId = "io.netty"; + sha512 = "f6d9c4a5b508ca0d5f0e213473088f5d7b2e184e447dc092e69227109e28da9b8e68b2238ca6ab4e9915bacacf59cc0dce6ebcbbb05dad34a03b7976d9670c51"; + version = "4.1.63.Final"; + + }; + paths = [ src ]; + } + + rec { + name = "ring-oauth2/ring-oauth2"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ring-oauth2"; + groupId = "ring-oauth2"; + sha512 = "3ed765b4bbb5749fcdcdb501b93ab656a413ade5af24c7aa34639718ed1fd0a5f325b05bd135540d56e55cbb456a2cb7852ba0e45bc5233e28229986eef75bb9"; + version = "0.2.0"; + + }; + paths = [ src ]; + } + + rec { + name = "tools.macro/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "tools.macro"; + groupId = "org.clojure"; + sha512 = "65ce5e29379620ac458274c53cd9926e4b764fcaebb1a2b3bc8aef86bbe10c79e654b028bc4328905d2495a680fa90f5002cf5c47885f6449fad43a04a594b26"; + version = "0.1.5"; + + }; + paths = [ src ]; + } + + rec { + name = "jackson-dataformat-cbor/com.fasterxml.jackson.dataformat"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jackson-dataformat-cbor"; + groupId = "com.fasterxml.jackson.dataformat"; + sha512 = "ea5d049eac1b94666479c5e36de14d8fa4b7f24cb92f0f310d2ec2b4de66ef9023161060e67228ef2d7420a002ef861db12a29cad0864638c21612da49686f4f"; + version = "2.12.4"; + + }; + paths = [ src ]; + } + + rec { + name = "depstar/seancorfield"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "depstar"; + groupId = "seancorfield"; + sha512 = "0f4458b39b8b1949755bc2fe64b239673a9efa3a0140998464bbbcab216ec847344c1b8920611f7c9ca07261850f3a08144ae221cc2c41813a080189e32f9c10"; + version = "1.0.94"; + + }; + paths = [ src ]; + } + + rec { + name = "logback-core/ch.qos.logback"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "logback-core"; + groupId = "ch.qos.logback"; + sha512 = "fc554548f499e284007eeecf76bf4e1995effb6ac8a6262aa96118f623bf9085a9d5bec3741833dd3cae6a76b2ff78c6d0a1fe68bc01213207c93d8e2da345ca"; + version = "1.3.0-alpha12"; + + }; + paths = [ src ]; + } + + rec { + name = "honeysql/honeysql"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "honeysql"; + groupId = "honeysql"; + sha512 = "74d1d93c968b33686848e3bf8934f3b5f002c2b69b1b55a3a3b172c952e9991324e6e95e3a0ce2fecf1de0d3a036f4dff7286df689f0733f253909464e0269f6"; + version = "1.0.461"; + + }; + paths = [ src ]; + } + + rec { + name = "netty-buffer/io.netty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "netty-buffer"; + groupId = "io.netty"; + sha512 = "181b55d99d8d46bbf5f67f05bdccb0381af23a9fca3e6d935e6cde727b132c67133de1c3d81ed19b04c1a5b232be0de16ec1de7e81b532878bc69564237c15dc"; + version = "4.1.63.Final"; + + }; + paths = [ src ]; + } + + rec { + name = "slingshot/slingshot"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "slingshot"; + groupId = "slingshot"; + sha512 = "ff2b2a27b441d230261c7f3ec8c38aa551865e05ab6438a74bd12bfcbc5f6bdc88199d42aaf5932b47df84f3d2700c8f514b9f4e9b5da28d29da7ff6b09a7fb5"; + version = "0.12.2"; + + }; + paths = [ src ]; + } + + rec { + name = "httpcore-nio/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpcore-nio"; + groupId = "org.apache.httpcomponents"; + sha512 = "002af5f72b68a4ff1b1ff46b788013283d195e1d62ee1d7b102aa930b30f77f7e215a6d18edbea0fccd18fb1fa3a66cc4aef6070d72d6d1886f0044dfe0e16c7"; + version = "4.4.10"; + + }; + paths = [ src ]; + } + + rec { + name = "ring-jetty-adapter/ring"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ring-jetty-adapter"; + groupId = "ring"; + sha512 = "93075903ad73a8b73cb77ee9f53ed33594f40a5dafe8129089adb4cfa333e37468764203c00244568f02abf0c0eee9f5d9a9f96c420919027cf2746a41ec38e3"; + version = "1.9.4"; + + }; + paths = [ src ]; + } + + rec { + name = "simpleclient_tracer_common/io.prometheus"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "simpleclient_tracer_common"; + groupId = "io.prometheus"; + sha512 = "6f717af63340efd84c5467ae752be7e66f586f0e8b57adb5b7a8ef99b223203ed829aad6797f6ef1811d6d861b00a621a1288c9271ec2ba77018d6d9eb9e7987"; + version = "0.12.0"; + + }; + paths = [ src ]; + } + + rec { + name = "component/com.stuartsierra"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "component"; + groupId = "com.stuartsierra"; + sha512 = "108b02f51165ad07c2cf5232fbd954d052880c2456e6fb6db3342bda6851c76b73bf9145f03fb0df2b5782fe39f368b2868780c1e8e2dfa2ab2c68dd97f34ab7"; + version = "1.0.0"; + + }; + paths = [ src ]; + } + + rec { + name = "netty-handler/io.netty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "netty-handler"; + groupId = "io.netty"; + sha512 = "48874727553dd7084f5c48d90de123704ae334837c3a103f598887bb21405dd62c57603b59300ac2fcdd936f0af99ed0730487fb9fb8917d236b8fe3f78f3c02"; + version = "4.1.63.Final"; + + }; + paths = [ src ]; + } + + rec { + name = "yuicompressor/com.yahoo.platform.yui"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "yuicompressor"; + groupId = "com.yahoo.platform.yui"; + sha512 = "ba2588bd50eaa3005b1919daad9f9c86a33351ceb9b7b5f0a9a498a548cc523e99f9345917a64303f8e23925feea226386d3eac01f640f788d1be4c7cf0315e0"; + version = "2.4.8"; + + }; + paths = [ src ]; + } + + rec { + name = "commons-io/commons-io"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-io"; + groupId = "commons-io"; + sha512 = "6af22dffaaecd1553147e788b5cf50368582318f396e456fe9ff33f5175836713a5d700e51720465c932c2b1987daa83027358005812d6a95d5755432de3a79d"; + version = "2.10.0"; + + }; + paths = [ src ]; + } + + rec { + name = "tools.namespace/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "tools.namespace"; + groupId = "org.clojure"; + sha512 = "2cdb9c5d9bc4fd01dae182e9ad4b91eeaa2487003a977e7d8d5e66f562a9544b59f558710eccf421ea63cbbfa953ac8944fe9b9a76049fb82a47eb2bdcb3a4d7"; + version = "1.1.1"; + + }; + paths = [ src ]; + } + + rec { + name = "honeysql/com.github.seancorfield"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "honeysql"; + groupId = "com.github.seancorfield"; + sha512 = "a0e5ebbf922aaf170c2d74ec0efc0df7e3bda92d0b8cc5f40ee4c8ddcb8c7e0e46556fac381513e0ac76b10f681c14c2d2569010c2f8eab4ff04f6373c2bf229"; + version = "2.2.840"; + + }; + paths = [ src ]; + } + + rec { + name = "jackson-core/com.fasterxml.jackson.core"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jackson-core"; + groupId = "com.fasterxml.jackson.core"; + sha512 = "428e0ebb16dd4c74ab0adf712058fd0dc0cd788f6e6f90c60c627da6577b345fac60a30694e111f1cd4e3e8bf79a1f1b820d30ada114984b26c28e299e326eaa"; + version = "2.12.4"; + + }; + paths = [ src ]; + } + + rec { + name = "clj-time/clj-time"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clj-time"; + groupId = "clj-time"; + sha512 = "cfeb46af59fd4112aa5a5d0087a39355f0fc19514b4c02bc6c3d9f81c9bda40491686207836e9a7943aebeb82a3b36f4e8b7407a8908c5ef151122644b278d75"; + version = "0.15.2"; + + }; + paths = [ src ]; + } + + rec { + name = "clj-http/clj-http"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clj-http"; + groupId = "clj-http"; + sha512 = "9884557d4f38068cb3234aec80acc0de8f9716645529693ffd9bd6db8221f5d1cf9e2d1b8bf7c7df4215d71372b02d83043ebf8fc27dc422552b32c9bdba1602"; + version = "3.12.3"; + + }; + paths = [ src ]; + } + + rec { + name = "jul-to-slf4j/org.slf4j"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jul-to-slf4j"; + groupId = "org.slf4j"; + sha512 = "350cfb889248d724b27dce697f635f12d9db463f107830b9518ce184dc4cc1ab3933eb5bdab08515e69766c3d5be24547dac289d6406c44eca90717230714b91"; + version = "2.0.0-alpha4"; + + }; + paths = [ src ]; + } + + rec { + name = "migratus/migratus"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "migratus"; + groupId = "migratus"; + sha512 = "ee5ce8601930d063e0d9d90fc8e165b78fc1587bfd7e0fc9922735bc2f9fc27f8cf8bf10d49d6fd57b899ac4b250145bd653915ed770424416e026ba37d1b604"; + version = "1.3.5"; + + }; + paths = [ src ]; + } + + rec { + name = "httpcore/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpcore"; + groupId = "org.apache.httpcomponents"; + sha512 = "f16a652f4a7b87dbf7cb16f8590d54a3f719c4c7b2f8883ce59db2d73be4701b64f2ca8a2c45aca6a5dbeaddeedff0c280a03722f70c076e239b645faa54eff9"; + version = "4.4.14"; + + }; + paths = [ src ]; + } + + rec { + name = "httpclient-cache/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpclient-cache"; + groupId = "org.apache.httpcomponents"; + sha512 = "e150e8dc49c8c9972d8b324b56bb292b15e2f0e686f1292c4edac975615dfb16e5edb8ab325e614732a7d43a03061ca4fe93fe1e1f7487851a4d4d3af50a61f9"; + version = "4.5.13"; + + }; + paths = [ src ]; + } + + rec { + name = "instaparse/instaparse"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "instaparse"; + groupId = "instaparse"; + sha512 = "ec2fcf4a09319a8fa9489b08fd9c9a5fe6e63155dde74d096f947fabc4f68d3d1bf68faf21e175e80eaee785f563a1903d30c550f93fb13a16a240609e3dfa2e"; + version = "1.4.8"; + + }; + paths = [ src ]; + } + + rec { + name = "honeysql-postgres/nilenso"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "honeysql-postgres"; + groupId = "nilenso"; + sha512 = "d4accd3b8819cf715ecdb29496cf5a6a5ad3871fd579e55c7148d4e05774cb896c681b0c6f84df88aa9cd8e6ef9bfd65788ede9a49ba365ad0e32ee350091879"; + version = "0.4.112"; + + }; + paths = [ src ]; + } + + rec { + name = "clj-tuple/clj-tuple"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clj-tuple"; + groupId = "clj-tuple"; + sha512 = "dd626944d0aba679a21b164ed0c77ea84449359361496cba810f83b9fdeab751e5889963888098ce4bf8afa112dbda0a46ed60348a9c01ad36a2e255deb7ab6d"; + version = "0.2.2"; + + }; + paths = [ src ]; + } + + rec { + name = "jackson-annotations/com.fasterxml.jackson.core"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jackson-annotations"; + groupId = "com.fasterxml.jackson.core"; + sha512 = "6fdad6c5bb71a97331a662fe26265aacab6869f3307a710697d5c2f256fd48935764bfb0b3505a2cbb1605daf0b7350abdf84a1b1cf2bb1e91d9184565243c8e"; + version = "2.12.4"; + + }; + paths = [ src ]; + } + + rec { + name = "hiccup/hiccup"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "hiccup"; + groupId = "hiccup"; + sha512 = "034f15be46c35029f41869c912f82cb2929fbbb0524ea64bd98dcdb9cf09875b28c75e926fa5fff53942b0f9e543e85a73a2d03c3f2112eecae30fcef8b148f4"; + version = "1.0.5"; + + }; + paths = [ src ]; + } + + rec { + name = "riddley/riddley"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "riddley"; + groupId = "riddley"; + sha512 = "b478ecba9d1ab9d38c84a42354586fcece763000907b40c97bc43c0f16dc560b0860144efe410193cb3b7cb0149fbc1724fdd737cc3ba53de23618f5b30e6f9f"; + version = "0.1.12"; + + }; + paths = [ src ]; + } + + rec { + name = "java.classpath/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "java.classpath"; + groupId = "org.clojure"; + sha512 = "90cd8edeaea02bd908d8cfb0cf5b1cf901aeb38ea3f4971c4b813d33210438aae6fff8e724a8272d2ea9441d373e7d936fa5870e309c1e9721299f662dbbdb9a"; + version = "1.0.0"; + + }; + paths = [ src ]; + } + + rec { + name = "simpleclient_pushgateway/io.prometheus"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "simpleclient_pushgateway"; + groupId = "io.prometheus"; + sha512 = "31c8878929f516ba7030cc9ec4ac4cbcb09955a9fdae23c6904bc481e40e70e1b3e05619c49b646119077ef6f57c430cc7944f6bafdbca24c9efa8145474fcf7"; + version = "0.12.0"; + + }; + paths = [ src ]; + } + + rec { + name = "ns-tracker/ns-tracker"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ns-tracker"; + groupId = "ns-tracker"; + sha512 = "cfb6c2c9f899b43d1284acdc572b34b977936c4df734b38137dfea045421b74d529509cde23695f1dc5ee06d046c2f6b61a2cd98058da1c7220c21dd0361964f"; + version = "0.4.0"; + + }; + paths = [ src ]; + } + + rec { + name = "clout/clout"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clout"; + groupId = "clout"; + sha512 = "99d6e1a8c5726ca4e5d12b280a39e6d1182d734922600f27d588d3d65fbc830c5e03f9e0421ff25c819deee4d1f389fd3906222716ace1eb17ce70ef9c5e8f4b"; + version = "2.2.1"; + + }; + paths = [ src ]; + } + + rec { + name = "commons-logging/commons-logging"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-logging"; + groupId = "commons-logging"; + sha512 = "ed00dbfabd9ae00efa26dd400983601d076fe36408b7d6520084b447e5d1fa527ce65bd6afdcb58506c3a808323d28e88f26cb99c6f5db9ff64f6525ecdfa557"; + version = "1.2"; + + }; + paths = [ src ]; + } + + rec { + name = "clojure.java-time/clojure.java-time"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clojure.java-time"; + groupId = "clojure.java-time"; + sha512 = "62d8a286ec3393594e7f84eba22dbb02c1305a80a18b2574058ae963d3f3e829ff960c8b66e89069e6c071a11f869203134c6c4cdec6f8e516c9b314796c8108"; + version = "0.3.3"; + + }; + paths = [ src ]; + } + + rec { + name = "data.csv/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "data.csv"; + groupId = "org.clojure"; + sha512 = "b039775a859ed27eca8f8ae74ccb6afde3ad1fe2b3cbe542240c324d60fe1237e495eb1300ee9eb4ff4ef59f01faf7aec6ef1dd6a025ee4fe556c1d91acfcf1b"; + version = "1.0.0"; + + }; + paths = [ src ]; + } + + rec { + name = "simpleclient_tracer_otel_agent/io.prometheus"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "simpleclient_tracer_otel_agent"; + groupId = "io.prometheus"; + sha512 = "97694210d9a5b48a7cb9dda2a187432c4813edb3051edfa5832a0a471e0b2d5988dab92b70c292e78f59b169345deb5c1c706361fd726f3dc2480766dedfdcec"; + version = "0.12.0"; + + }; + paths = [ src ]; + } + + rec { + name = "next.jdbc/com.github.seancorfield"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "next.jdbc"; + groupId = "com.github.seancorfield"; + sha512 = "0b4b01ba126bb8b1e2c14262db9fca75456b274d09535d9a7bb386699bf20dc9ac11590d210769e7429ca59ebfdfbb06916b3ff275cc817d74eac5bbabdab8f2"; + version = "1.2.761"; + + }; + paths = [ src ]; + } + + rec { + name = "java.jdbc/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "java.jdbc"; + groupId = "org.clojure"; + sha512 = "6162b7774dca58b62a94bc5a04ba845e4c7065c9c589cc3bb802becfec0baf0989a338c1bf9a5db7c3128873702840d5f2451628f3aac977245975d65a683b7d"; + version = "0.7.11"; + + }; + paths = [ src ]; + } + + rec { + name = "netty-transport/io.netty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "netty-transport"; + groupId = "io.netty"; + sha512 = "c11d690ffeaf3267b2166f73a43108fb89d588fcef3f6d3053bf4b6f6669483baa618fd97438010692a6fa28334372d5a31b7c0996961d4eabb60cbdc358a536"; + version = "4.1.63.Final"; + + }; + paths = [ src ]; + } + + rec { + name = "crypto-random/crypto-random"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "crypto-random"; + groupId = "crypto-random"; + sha512 = "3520df744f250dbe061d1a5d7a05b7143f3a67a4c3f9ad87b8044ee68a36a702a0bcb3a203e35d380899dd01c28e01988b0a7af914b942ccbe0c35c9bdb22e11"; + version = "1.2.1"; + + }; + paths = [ src ]; + } + + rec { + name = "netty-transport-native-unix-common/io.netty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "netty-transport-native-unix-common"; + groupId = "io.netty"; + sha512 = "b63e5f8a44b7f37f3dba378bd06af64dd1d7be3f0b1a7d47ad139ff06e0212b4c7081275b1b5b12183aeb72eb5f9bf9ef03ed8c78bc302aeb4817dca7bd89f3a"; + version = "4.1.63.Final"; + + }; + paths = [ src ]; + } + + rec { + name = "ring-codec/ring"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ring-codec"; + groupId = "ring"; + sha512 = "38b9775a794831b8afd8d66991a75aa5910cd50952c9035866bf9cc01353810aedafbc3f35d8f9e56981ebf9e5c37c00b968759ed087d2855348b3f46d8d0487"; + version = "1.1.3"; + + }; + paths = [ src ]; + } + + rec { + name = "spy/com.impossibl.pgjdbc-ng"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "spy"; + groupId = "com.impossibl.pgjdbc-ng"; + sha512 = "173615c39aa6015a732e329217b40e3ea1c304c9c168d2764d6ef23ab8775e2f4432339bc22d049662561f09d3fd890b5415738620d64dcedb762d5da26b4ebb"; + version = "0.8.9"; + + }; + paths = [ src ]; + } + + rec { + name = "logback-json-core/ch.qos.logback.contrib"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "logback-json-core"; + groupId = "ch.qos.logback.contrib"; + sha512 = "2a826036f21997e2979fda83ae3e33cf62f3b2b2df15a7b11d1fd8a52163b09f0f2f8d72f5fdcea0ec1289b3d27727ed5e6b0bcdf4c5d741f4bac07b7b6139e8"; + version = "0.1.5"; + + }; + paths = [ src ]; + } + + rec { + name = "httpclient/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpclient"; + groupId = "org.apache.httpcomponents"; + sha512 = "3567739186e551f84cad3e4b6b270c5b8b19aba297675a96bcdff3663ff7d20d188611d21f675fe5ff1bfd7d8ca31362070910d7b92ab1b699872a120aa6f089"; + version = "4.5.13"; + + }; + paths = [ src ]; + } + + rec { + name = "crypto-equality/crypto-equality"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "crypto-equality"; + groupId = "crypto-equality"; + sha512 = "54cf3bd28f633665962bf6b41f5ccbf2634d0db210a739e10a7b12f635e13c7ef532efe1d5d8c0120bb46478bbd08000b179f4c2dd52123242dab79fea97d6a6"; + version = "1.0.0"; + + }; + paths = [ src ]; + } + + rec { + name = "cheshire/cheshire"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "cheshire"; + groupId = "cheshire"; + sha512 = "855e9c42a8d1c64f4db5cda45e31e914eb5ed99a715e8d7a5759a9c4ab6c69a82353635ca7b0837880c6cf9b41b11184ae11e09cbf2c07aa13db32c539e5dfd4"; + version = "5.10.1"; + + }; + paths = [ src ]; + } + + rec { + name = "tigris/tigris"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "tigris"; + groupId = "tigris"; + sha512 = "fdff4ef5e7175a973aaef98de4f37dee8e125fc711c495382e280aaf3e11341fe8925d52567ca60f3f1795511ade11bc23461c88959632dfae3cf50374d02bf6"; + version = "0.1.2"; + + }; + paths = [ src ]; + } + + rec { + name = "config/yogthos"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "config"; + groupId = "yogthos"; + sha512 = "3437992d192465edc74aec5259d5e0c0ad7e631dff860b2ee14cef27f13cee7c60487202cf00fc160a95fb0b85ce1ddf56cbdd0c008b47ac598061bf115f6a23"; + version = "1.1.9"; + + }; + paths = [ src ]; + } + + rec { + name = "jetty-io/org.eclipse.jetty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jetty-io"; + groupId = "org.eclipse.jetty"; + sha512 = "a8c5f73089daa0c8b27f836acddf40bcbf07bbb2571a4d73653be8aac3fb339022f546326722f216bad78a68886934d24db9bec54235124592dd29dbeab69051"; + version = "9.4.42.v20210604"; + + }; + paths = [ src ]; + } + + rec { + name = "logback-json-classic/ch.qos.logback.contrib"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "logback-json-classic"; + groupId = "ch.qos.logback.contrib"; + sha512 = "d30bf70217d316914d83d46cc15783f656354084087d59cbc0620a746f10b4a42e56d33b3e50a8b3596a64ec8314730bf5ff9a3f7dc3417bdd0582665be009ec"; + version = "0.1.5"; + + }; + paths = [ src ]; + } + + rec { + name = "tools.reader/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "tools.reader"; + groupId = "org.clojure"; + sha512 = "3481259c7a1eac719db2921e60173686726a0c2b65879d51a64d516a37f6120db8ffbb74b8bd273404285d7b25143ab5c7ced37e7c0eaf4ab1e44586ccd3c651"; + version = "1.3.6"; + + }; + paths = [ src ]; + } + + rec { + name = "simpleclient_common/io.prometheus"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "simpleclient_common"; + groupId = "io.prometheus"; + sha512 = "dedd003638eb3651c112e2d697ac94eb4e3b3e32c94fa41bb1efe2c889a347cdc7bd13256e05423f3370592d4fd65faf8db57f0387ab75814d7fa77b14cbbadf"; + version = "0.12.0"; + + }; + paths = [ src ]; + } + + rec { + name = "commons-compiler/org.codehaus.janino"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-compiler"; + groupId = "org.codehaus.janino"; + sha512 = "f0778b891ef14d8ee6776747eab0b25da716cdc530752a81aedec2a77570e2f66402179b9408a6efde8125c808eb060a720d2f4977c1f1d022bdaae7eac8d011"; + version = "3.1.2"; + + }; + paths = [ src ]; + } + + rec { + name = "servlet-api/javax.servlet"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "servlet-api"; + groupId = "javax.servlet"; + sha512 = "363ba5590436ab82067b7a2e14b481aeb2b12ca4048d7a1519a2e549b2d3c09ddf718ac64dc2be6c2fc24c51fdc9c8160261329403113369588ce27d87771db6"; + version = "2.5"; + + }; + paths = [ src ]; + } + + rec { + name = "iapetos/clj-commons"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "iapetos"; + groupId = "clj-commons"; + sha512 = "d17f36c0cf0ec78db5e893e5c033f8562b31650bda6f5ee582e68f84a07a3631d04d6f69e4e18b1ca64e732c180fa669dfb69a78849e13f601cd563a7a8aab94"; + version = "0.1.12"; + + }; + paths = [ src ]; + } + + rec { + name = "javax.servlet-api/javax.servlet"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "javax.servlet-api"; + groupId = "javax.servlet"; + sha512 = "32f7e3565c6cdf3d9a562f8fd597fe5059af0cf6b05b772a144a74bbc95927ac275eb38374538ec1c72adcce4c8e1e2c9f774a7b545db56b8085af0065e4a1e5"; + version = "3.1.0"; + + }; + paths = [ src ]; + } + + rec { + name = "potemkin/potemkin"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "potemkin"; + groupId = "potemkin"; + sha512 = "5abc050bf7ff0b27d8c45aaa5e378201980815b711b2db99735db73304576c17e285026ea48a714bf0b0df7ad7a008de38b7d182cdc0e8989f4be1e6b3afa8aa"; + version = "0.4.5"; + + }; + paths = [ src ]; + } + + rec { + name = "netty-resolver/io.netty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "netty-resolver"; + groupId = "io.netty"; + sha512 = "fabf893de74264caa1799c15d184ed8f20b7bf9b1c41abb29f29adf728a934951f97892a4924634f9efbda17c8cf74ea3ff97bafca616711e3c5f79b8ed9ef3e"; + version = "4.1.63.Final"; + + }; + paths = [ src ]; + } + + rec { + name = "netty-transport-native-epoll/io.netty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "netty-transport-native-epoll"; + groupId = "io.netty"; + sha512 = "6fbc2dd2622699f3fc1f329acbd94baf7f1d8923c5cfcae262e6f2d64b4fd71b606561bce5e2b511dff8e052cdade930091fab683fd98713f6b62a622a2c6254"; + version = "4.1.63.Final"; + + }; + paths = [ src ]; + } + + rec { + name = "clj-stacktrace/clj-stacktrace"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clj-stacktrace"; + groupId = "clj-stacktrace"; + sha512 = "993f8a544203801fc074eefacee8e553e426422b3492d47b857d87ac73cde72c91e29f629382b9eae8cf9600bc2c4c29d2e7169e509c46302ab973c86e73af0c"; + version = "0.2.8"; + + }; + paths = [ src ]; + } + + rec { + name = "cambium.codec-cheshire/cambium"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "cambium.codec-cheshire"; + groupId = "cambium"; + sha512 = "614491cf752a597f29ae29885db6c1ed191341303d89183bee52e4e2c76eb8eb14693562ad09484f379a074b36d97085e848ec3845e069440e6422506c1636f1"; + version = "1.0.0"; + + }; + paths = [ src ]; + } + + rec { + name = "slf4j-api/org.slf4j"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "slf4j-api"; + groupId = "org.slf4j"; + sha512 = "ad705ab6fd5cd904ef6861c0adf08af19593cf6a486b18de548fe3d68e57b1baa7e02947584fd4dcc350ddcddcf906c01e8d9ba7943a202690d0d788627696b5"; + version = "2.0.0-alpha4"; + + }; + paths = [ src ]; + } + + rec { + name = "test.check/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "test.check"; + groupId = "org.clojure"; + sha512 = "b8d7a330b0b5514cd6a00c4382052fab51c3c9d3bc53133f8506791fa670e7c5ecd65094977ea5ced91f59623b0abd1ab8feeec96d63c5c6e459b265a655c577"; + version = "1.1.1"; + + }; + paths = [ src ]; + } + + rec { + name = "ring-logger/ring-logger"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ring-logger"; + groupId = "ring-logger"; + sha512 = "b675a61c173289fc610d84920ba40178bf62b3bc680923cb66866d78ee2a508296b27a1ab14b66bfbe0304a64166a7e3c3ddee36564dd4a2f988861bce455a3a"; + version = "1.0.1"; + + }; + paths = [ src ]; + } + + rec { + name = "ring-servlet/ring"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ring-servlet"; + groupId = "ring"; + sha512 = "3d8e6ec224e13d54810a945c0b6c0d2d863736a48d8c4bfc8fadb96b6b0fa9baa638644d0d92d8a53650b188e6e75d391731b08b26eb0f551e90a7504e7f4267"; + version = "1.9.4"; + + }; + paths = [ src ]; + } + + rec { + name = "logback-classic/ch.qos.logback"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "logback-classic"; + groupId = "ch.qos.logback"; + sha512 = "f9fe0f126061f4abe3973b631b8d8244ba9e9d77783479a6500d629d772050dee508a001fc14d2131407fbdd0d33dd6b8aeb9b1ea9125b471bb8412e8de659e6"; + version = "1.3.0-alpha12"; + + }; + paths = [ src ]; + } + + rec { + name = "dependency/com.stuartsierra"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "dependency"; + groupId = "com.stuartsierra"; + sha512 = "d32fbc4813bd16f2ed8c82e2915e1fb564e88422159bd3580a85c8cd969d1bbbe315bdc13d29c2f0eaceeeafcf649ee712c8df4532464d560aaeae4ae5953866"; + version = "1.0.0"; + + }; + paths = [ src ]; + } + + rec { + name = "camel-snake-kebab/camel-snake-kebab"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "camel-snake-kebab"; + groupId = "camel-snake-kebab"; + sha512 = "589d34b500560b7113760a16bfb6f0ccd8f162a1ce8c9bc829495432159ba9c95aebf6bc43aa126237a0525806a205a05f9910122074902b659e7fd151d176b1"; + version = "0.4.2"; + + }; + paths = [ src ]; + } + + rec { + name = "ring/ring"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ring"; + groupId = "ring"; + sha512 = "93c48fb670736b91fb41d8076e1e9c4f53c67693d15e75290da319e7d7881b829a24180029b3a0fa051473c6c77ac3c97b519254ebf2b2c9538b185e79b69162"; + version = "1.9.4"; + + }; + paths = [ src ]; + } + + rec { + name = "netty-transport-native-kqueue/io.netty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "netty-transport-native-kqueue"; + groupId = "io.netty"; + sha512 = "87e10c06e394a1698d65381d3be8336f753c55e3e899e297510161d0c72540023f30f9032322957e035ead793204a084b988bc21a2bc312fcf7567a22d02a3c4"; + version = "4.1.63.Final"; + + }; + paths = [ src ]; + } + + rec { + name = "java.data/org.clojure"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "java.data"; + groupId = "org.clojure"; + sha512 = "225e1eafd1a659278212d831f7cd8609359f8c880ef3d69b4ade6301ce3c511307ce31d94cb82d5407314b990bd04714ec26273bb3036b248116a7a75fa75e1f"; + version = "1.0.95"; + + }; + paths = [ src ]; + } + + rec { + name = "jetty-server/org.eclipse.jetty"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jetty-server"; + groupId = "org.eclipse.jetty"; + sha512 = "b347f8a6e5b84e0f460037027e238a61edec710ade768c95e7be13dcea498abe43d5e622ee69ac7494138d1a8fcf92e07b7deab569c554831c57baad71c53b9b"; + version = "9.4.42.v20210604"; + + }; + paths = [ src ]; + } + + rec { + name = "httpmime/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpmime"; + groupId = "org.apache.httpcomponents"; + sha512 = "e1b0ee84bce78576074dc1b6836a69d8f5518eade38562e6890e3ddaa72b7f54bf735c8e2286142c58cddf45f745da31261e5d73b7d8092eb6ecfb20946eb36c"; + version = "4.5.13"; + + }; + paths = [ src ]; + } + + rec { + name = "log4j-over-slf4j/org.slf4j"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "log4j-over-slf4j"; + groupId = "org.slf4j"; + sha512 = "48fa023c57294b73b9bd2f53e3dd3169e03426e5b3aa9d80e1bb1a9abf927fc26ef9f64d02b9769d5577d83094d0f41f044d35bb3b4f6037d66d6b2f19b484a1"; + version = "2.0.0-alpha4"; + + }; + paths = [ src ]; + } + + rec { + name = "ring-core/ring"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ring-core"; + groupId = "ring"; + sha512 = "38d7214a3fc1b80ab55999036638dd1971272e01bec4cb8e0ee0a4aa83f51b8c41ba8a5850b0660227f067d2f9c6d75c0c0737725ea02762bbf8d192dc72febe"; + version = "1.9.4"; + + }; + paths = [ src ]; + } + + rec { + name = "cambium.core/cambium"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "cambium.core"; + groupId = "cambium"; + sha512 = "0e1fe626c6d0b31aad84ea2e4466273065925548ee5915f442b7997ebfe795faea36dbeac50a0f8c16bbd20d877511e3f8c4ff4f2b916a4538513aaa5cc20112"; + version = "1.1.1"; + + }; + paths = [ src ]; + } + + rec { + name = "medley/medley"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "medley"; + groupId = "medley"; + sha512 = "749ef43b5ea2cae7dc96db871cdd15c7b3c9cfbd96828c20ab08e67d39a5e938357d15994d8d413bc68678285d6c666f2a7296fbf305706d03b3007254e3c55c"; + version = "1.3.0"; + + }; + paths = [ src ]; + } + + rec { + name = "garden/garden"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "garden"; + groupId = "garden"; + sha512 = "2cc29f071b68bf451835f76de351ac2efb930b5df9ca7237fdca439d3c4d797d7fa207a147886efe1738ab1c50b76c1e366bf9ffcd6f286b0b211260aedd0b25"; + version = "1.3.10"; + + }; + paths = [ src ]; + } + + rec { + name = "jackson-dataformat-smile/com.fasterxml.jackson.dataformat"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jackson-dataformat-smile"; + groupId = "com.fasterxml.jackson.dataformat"; + sha512 = "69676964a2b09516b8ffd0d847b6f9a9b843424185453731b548c25e7e9ce30e808c56d66923f9183e2b5c1ba007421b146a6806e768b8e6b07470d60227f1dd"; + version = "2.12.4"; + + }; + paths = [ src ]; + } + + rec { + name = "jaxb-api/javax.xml.bind"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "jaxb-api"; + groupId = "javax.xml.bind"; + sha512 = "0c5bfc2c9f655bf5e6d596e0c196dcb9344d6dc78bf774207c8f8b6be59f69addf2b3121e81491983eff648dfbd55002b9878132de190825dad3ef3a1265b367"; + version = "2.3.0"; + + }; + paths = [ src ]; + } + + rec { + name = "pgjdbc-ng/com.impossibl.pgjdbc-ng"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "pgjdbc-ng"; + groupId = "com.impossibl.pgjdbc-ng"; + sha512 = "a34ac9146257329f6e9b354f13f564c65dbea6463addae383e3918d3a64c90c67f5f7fda6b5c3866de991a568d6690edb3fb09f2507593390a6e30ec0c79e02c"; + version = "0.8.9"; + + }; + paths = [ src ]; + } + + rec { + name = "http-kit/http-kit"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "http-kit"; + groupId = "http-kit"; + sha512 = "4186a2429984745e18730aa8fd545f1fc1812083819ebf77aecfc04e0d31585358a5e25a308c7f21d81359418bbc72390c281f5ed91ae116cf1af79860ba22c3"; + version = "2.5.3"; + + }; + paths = [ src ]; + } + + ]; +} + \ No newline at end of file diff --git a/users/aspen/bbbg/env/dev/bbbg-signup/env.clj b/users/aspen/bbbg/env/dev/bbbg-signup/env.clj new file mode 100644 index 000000000000..c30e328ffa24 --- /dev/null +++ b/users/aspen/bbbg/env/dev/bbbg-signup/env.clj @@ -0,0 +1,3 @@ +(ns bbbg.env) + +(def environment :env/dev) diff --git a/users/aspen/bbbg/env/dev/logback.xml b/users/aspen/bbbg/env/dev/logback.xml new file mode 100644 index 000000000000..7aa21978bbe7 --- /dev/null +++ b/users/aspen/bbbg/env/dev/logback.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg { %mdc }%n</pattern> + </encoder> + </appender> + + <root level="INFO"> + <appender-ref ref="STDOUT" /> + </root> + + <logger name="user" level="ALL" /> + <logger name="ci.windtunnel" level="ALL" /> +</configuration> diff --git a/users/aspen/bbbg/env/prod/bbbg-signup/env.clj b/users/aspen/bbbg/env/prod/bbbg-signup/env.clj new file mode 100644 index 000000000000..46e8cd67e318 --- /dev/null +++ b/users/aspen/bbbg/env/prod/bbbg-signup/env.clj @@ -0,0 +1,3 @@ +(ns bbbg.env) + +(def environment :env/prod) diff --git a/users/aspen/bbbg/env/prod/logback.xml b/users/aspen/bbbg/env/prod/logback.xml new file mode 100644 index 000000000000..b81118ed6b32 --- /dev/null +++ b/users/aspen/bbbg/env/prod/logback.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <!-- Silence Logback's own status messages about config parsing --> + <statusListener class="ch.qos.logback.core.status.NopStatusListener" /> + + <!-- Console output --> + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <!-- Only log level INFO and above --> + <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> + <level>INFO</level> + </filter> + <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> + <layout class="cambium.logback.json.FlatJsonLayout"> + <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter"> + <prettyPrint>false</prettyPrint> + </jsonFormatter> + <!-- <context>api</context> --> + <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</timestampFormat> + <timestampFormatTimezoneId>UTC</timestampFormatTimezoneId> + <appendLineSeparator>true</appendLineSeparator> + </layout> + </encoder> + </appender> + + + <root level="INFO"> + <appender-ref ref="STDOUT" /> + </root> + + <logger name="user" level="ALL" /> +</configuration> diff --git a/users/aspen/bbbg/env/test/bbbg-signup/env.clj b/users/aspen/bbbg/env/test/bbbg-signup/env.clj new file mode 100644 index 000000000000..352147a6d0fd --- /dev/null +++ b/users/aspen/bbbg/env/test/bbbg-signup/env.clj @@ -0,0 +1,3 @@ +(ns bbbg.env) + +(def environment :env/test) diff --git a/users/aspen/bbbg/env/test/logback.xml b/users/aspen/bbbg/env/test/logback.xml new file mode 100644 index 000000000000..8554f3d0ed0b --- /dev/null +++ b/users/aspen/bbbg/env/test/logback.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> + <pattern>%msg%n</pattern> + </encoder> + </appender> + <root level="OFF"> + <appender-ref ref="CONSOLE"/> + </root> +</configuration> diff --git a/users/aspen/bbbg/module.nix b/users/aspen/bbbg/module.nix new file mode 100644 index 000000000000..c5bacdf4d70d --- /dev/null +++ b/users/aspen/bbbg/module.nix @@ -0,0 +1,137 @@ +{ config, lib, pkgs, depot, ... }: + +let + bbbg = depot.users.aspen.bbbg; + cfg = config.services.bbbg; +in +{ + options = with lib; { + services.bbbg = { + enable = mkEnableOption "BBBG Server"; + + port = mkOption { + type = types.int; + default = 7222; + description = "Port to listen to for the HTTP server"; + }; + + domain = mkOption { + type = types.str; + default = "bbbg.gws.fyi"; + description = "Domain to host under"; + }; + + proxy = { + enable = mkEnableOption "NGINX reverse proxy"; + }; + + database = { + enable = mkEnableOption "BBBG Database Server"; + + user = mkOption { + type = types.str; + default = "bbbg"; + description = "Database username"; + }; + + host = mkOption { + type = types.str; + default = "localhost"; + description = "Database host"; + }; + + name = mkOption { + type = types.str; + default = "bbbg"; + description = "Database name"; + }; + + port = mkOption { + type = types.int; + default = 5432; + description = "Database host"; + }; + }; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.enable { + systemd.services.bbbg-server = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + DynamicUser = true; + Restart = "always"; + EnvironmentFile = config.age.secretsDir + "/bbbg"; + }; + + environment = { + PGHOST = cfg.database.host; + PGUSER = cfg.database.user; + PGDATABASE = cfg.database.name; + PORT = toString cfg.port; + BASE_URL = "https://${cfg.domain}"; + }; + + script = "${bbbg.server}/bin/bbbg-server"; + }; + + systemd.services.migrate-bbbg = { + description = "Run database migrations for BBBG"; + wantedBy = [ "bbbg-server.service" ]; + after = ([ "network.target" ] + ++ (if cfg.database.enable + then [ "postgresql.service" ] + else [ ])); + + serviceConfig = { + Type = "oneshot"; + EnvironmentFile = config.age.secretsDir + "/bbbg"; + }; + + environment = { + PGHOST = cfg.database.host; + PGUSER = cfg.database.user; + PGDATABASE = cfg.database.name; + }; + + script = "${bbbg.db-util}/bin/bbbg-db-util migrate"; + }; + }) + (lib.mkIf cfg.database.enable { + services.postgresql = { + enable = true; + authentication = lib.mkForce '' + local all all trust + host all all 127.0.0.1/32 password + host all all ::1/128 password + hostnossl all all 127.0.0.1/32 password + hostnossl all all ::1/128 password + ''; + + ensureDatabases = [ + cfg.database.name + ]; + + ensureUsers = [{ + name = cfg.database.user; + ensurePermissions = { + "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; + }; + }]; + }; + }) + (lib.mkIf cfg.proxy.enable { + services.nginx = { + enable = true; + virtualHosts."${cfg.domain}" = { + enableACME = true; + forceSSL = true; + locations."/".proxyPass = "http://localhost:${toString cfg.port}"; + }; + }; + }) + ]; +} diff --git a/users/aspen/bbbg/pom.xml b/users/aspen/bbbg/pom.xml new file mode 100644 index 000000000000..012c0985f12f --- /dev/null +++ b/users/aspen/bbbg/pom.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>fyi.gws</groupId> + <artifactId>bbbg</artifactId> + <version>0.1.0-SNAPSHOT</version> + <name>fyi.gws/bbbg</name> + <description>webhook listener for per-branch deploys</description> + <url>https://bbbg.gws.fyi</url> + <developers> + <developer> + <name>Griffin Smith</name> + </developer> + </developers> + <dependencies> + <dependency> + <groupId>org.clojure</groupId> + <artifactId>clojure</artifactId> + <version>1.11.0-alpha3</version> + </dependency> + </dependencies> + <build> + <sourceDirectory>src</sourceDirectory> + </build> + <repositories> + <repository> + <id>clojars</id> + <url>https://repo.clojars.org/</url> + </repository> + <repository> + <id>sonatype</id> + <url>https://oss.sonatype.org/content/repositories/snapshots/</url> + </repository> + </repositories> + <distributionManagement> + <repository> + <id>clojars</id> + <name>Clojars repository</name> + <url>https://clojars.org/repo</url> + </repository> + </distributionManagement> +</project> diff --git a/users/aspen/bbbg/resources/base.css b/users/aspen/bbbg/resources/base.css new file mode 100644 index 000000000000..c86c3f24f009 --- /dev/null +++ b/users/aspen/bbbg/resources/base.css @@ -0,0 +1,152 @@ +/* montserrat-italic - latin */ +@font-face { + font-family: "Montserrat"; + font-style: italic; + font-weight: 400; + src: local("Montserrat Italic"), local("Montserrat-Italic"), + url("/fonts/montserrat-v15-latin-italic.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("/fonts/montserrat-v15-latin-italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} + +/* montserrat-regular - latin */ +@font-face { + font-family: "Montserrat"; + font-style: normal; + font-weight: 400; + src: local("Montserrat Regular"), local("Montserrat-Regular"), + url("/fonts/montserrat-v15-latin-regular.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("/fonts/montserrat-v15-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} + +/* montserrat-500 - latin */ +@font-face { + font-family: "Montserrat"; + font-style: normal; + font-weight: 500; + src: local("Montserrat Medium"), local("Montserrat-Medium"), + url("/fonts/montserrat-v15-latin-500.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("/fonts/montserrat-v15-latin-500.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} + +/* montserrat-500italic - latin */ +@font-face { + font-family: "Montserrat"; + font-style: italic; + font-weight: 500; + src: local("Montserrat Medium Italic"), local("Montserrat-MediumItalic"), + url("/fonts/montserrat-v15-latin-500italic.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("/fonts/montserrat-v15-latin-500italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} + +/* montserrat-600 - latin */ +@font-face { + font-family: "Montserrat"; + font-style: normal; + font-weight: 600; + src: local("Montserrat SemiBold"), local("Montserrat-SemiBold"), + url("/fonts/montserrat-v15-latin-600.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("/fonts/montserrat-v15-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} + +/* montserrat-800 - latin */ +@font-face { + font-family: "Montserrat"; + font-style: normal; + font-weight: 800; + src: local("Montserrat ExtraBold"), local("Montserrat-ExtraBold"), + url("/fonts/montserrat-v15-latin-800.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("/fonts/montserrat-v15-latin-800.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} + +/* montserrat-800italic - latin */ +@font-face { + font-family: "Montserrat"; + font-style: italic; + font-weight: 800; + src: local("Montserrat ExtraBold Italic"), local("Montserrat-ExtraBoldItalic"), + url("/fonts/montserrat-v15-latin-800italic.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("/fonts/montserrat-v15-latin-800italic.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} + +body { + width: 100%; + font-family: "Montserrat", Helvetica, sans-serif; + margin: 0; + box-sizing: border-box; +} + +*, +::before, +::after { + box-sizing: border-box; +} + +ul, +ol { + padding: 0; +} + +body, +h1, +h2, +h3, +h4, +p, +ul, +ol, +li, +figure, +figcaption, +blockquote, +dl, +dd { + margin: 0; +} + +body { + min-height: 100vh; + scroll-behavior: smooth; + text-rendering: optimizeSpeed; + line-height: 1.5; +} + +ul[class], +ol[class] { + list-style: none; +} + +a:not([class]) { + text-decoration-skip-ink: auto; +} + +img { + max-width: 100%; + display: block; +} + +article > * + * { + margin-top: 1em; +} + +input, +button, +textarea, +select { + font: inherit; +} + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/users/aspen/bbbg/resources/migrations/20211212165646-init-schema.down.sql b/users/aspen/bbbg/resources/migrations/20211212165646-init-schema.down.sql new file mode 100644 index 000000000000..69b818a4f4ab --- /dev/null +++ b/users/aspen/bbbg/resources/migrations/20211212165646-init-schema.down.sql @@ -0,0 +1,14 @@ +drop table "public"."user"; + +-- ;; + +drop table "public"."event_attendee"; + + +-- ;; + +drop table "public"."event"; + +-- ;; + +drop table "public"."attendee"; diff --git a/users/aspen/bbbg/resources/migrations/20211212165646-init-schema.up.sql b/users/aspen/bbbg/resources/migrations/20211212165646-init-schema.up.sql new file mode 100644 index 000000000000..9718d84748ae --- /dev/null +++ b/users/aspen/bbbg/resources/migrations/20211212165646-init-schema.up.sql @@ -0,0 +1,32 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +-- ;; +CREATE TABLE "attendee" ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "meetup_name" TEXT NOT NULL, + "discord_name" TEXT, + "meetup_user_id" TEXT, + "organizer_notes" TEXT NOT NULL DEFAULT '', + "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now() +); +-- ;; +CREATE TABLE "event" ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "date" DATE NOT NULL, + "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now() +); +-- ;; +CREATE TABLE "event_attendee" ( + "event_id" UUID NOT NULL REFERENCES "event" ("id"), + "attendee_id" UUID NOT NULL REFERENCES "attendee" ("id"), + "rsvpd_attending" BOOL, + "attended" BOOL, + "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(), + PRIMARY KEY ("event_id", "attendee_id") +); +-- ;; +CREATE TABLE "user" ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "username" TEXT NOT NULL, + "discord_user_id" TEXT NOT NULL, + "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now() +); diff --git a/users/aspen/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql b/users/aspen/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql new file mode 100644 index 000000000000..936abf6c7d19 --- /dev/null +++ b/users/aspen/bbbg/resources/migrations/20211220002229-add-attendee-checks.down.sql @@ -0,0 +1 @@ +DROP TABLE "attendee_check"; diff --git a/users/aspen/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql b/users/aspen/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql new file mode 100644 index 000000000000..5e82dcb1711c --- /dev/null +++ b/users/aspen/bbbg/resources/migrations/20211220002229-add-attendee-checks.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE attendee_check ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "attendee_id" UUID NOT NULL REFERENCES attendee ("id"), + "user_id" UUID NOT NULL REFERENCES "public"."user" ("id"), + "last_dose_at" DATE, + "checked_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now() +); diff --git a/users/aspen/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.down.sql b/users/aspen/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.down.sql new file mode 100644 index 000000000000..cbee0c00acd9 --- /dev/null +++ b/users/aspen/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.down.sql @@ -0,0 +1 @@ +drop index attendee_uniq_meetup_user_id; diff --git a/users/aspen/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.up.sql b/users/aspen/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.up.sql new file mode 100644 index 000000000000..5895cad56bdf --- /dev/null +++ b/users/aspen/bbbg/resources/migrations/20211224161028-add-attendee-unique-meetup-id.up.sql @@ -0,0 +1,2 @@ +create unique index "attendee_uniq_meetup_user_id" on attendee (meetup_user_id); +-- ;; diff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff new file mode 100644 index 000000000000..1c83d8518d3d --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff2 b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff2 new file mode 100644 index 000000000000..9dc5c7f158af --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500.woff2 Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff new file mode 100644 index 000000000000..71476d858fd5 --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff2 b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff2 new file mode 100644 index 000000000000..0fb9838c9d76 --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-500italic.woff2 Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff new file mode 100644 index 000000000000..e7f8a31ba35c --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff2 b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff2 new file mode 100644 index 000000000000..29cc1a973450 --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-600.woff2 Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff new file mode 100644 index 000000000000..79203dd7801d --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff2 b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff2 new file mode 100644 index 000000000000..0abb707aeddf --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800.woff2 Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff new file mode 100644 index 000000000000..65415571a7f4 --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff2 b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff2 new file mode 100644 index 000000000000..674e6eabe747 --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-800italic.woff2 Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff new file mode 100644 index 000000000000..67f1e85379c2 --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff2 b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff2 new file mode 100644 index 000000000000..469aede09c6b --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-italic.woff2 Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff new file mode 100644 index 000000000000..676a065e24ff --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff Binary files differdiff --git a/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff2 b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff2 new file mode 100644 index 000000000000..70788c273207 --- /dev/null +++ b/users/aspen/bbbg/resources/public/fonts/montserrat-v15-latin-regular.woff2 Binary files differdiff --git a/users/aspen/bbbg/resources/public/main.js b/users/aspen/bbbg/resources/public/main.js new file mode 100644 index 000000000000..87c0b64d0a37 --- /dev/null +++ b/users/aspen/bbbg/resources/public/main.js @@ -0,0 +1,73 @@ +window.onload = () => { + const input = document.getElementById("name-autocomplete"); + if (input != null) { + const attendeeList = document.getElementById("attendees-list"); + const filterAttendees = (filter) => { + if (filter == "") { + for (let elt of attendeeList.querySelectorAll("li")) { + elt.classList.remove("hidden"); + } + + return; + } + + let re = ""; + for (let c of filter) { + re += `${c}.*`; + } + let filterRe = new RegExp(re, "i"); + + for (let elt of attendeeList.querySelectorAll("li")) { + const attendee = JSON.parse(elt.dataset.attendee); + if (attendee["bbbg.attendee/meetup-name"].match(filterRe) == null) { + elt.classList.add("hidden"); + } else { + elt.classList.remove("hidden"); + } + } + }; + + const attendeeIDInput = document.getElementById("attendee-id"); + const submit = document.querySelector("#submit-button"); + const signupForm = document.getElementById("signup-form"); + + input.oninput = (e) => { + filterAttendees(e.target.value); + attendeeIDInput.value = null; + submit.classList.add("hidden"); + submit.setAttribute("disabled", "disabled"); + signupForm.setAttribute("disabled", "disabled"); + }; + + attendeeList.addEventListener("click", (e) => { + if (!(e.target instanceof HTMLLIElement)) { + return; + } + if (e.target.dataset.attendee == null) { + return; + } + + const attendee = JSON.parse(e.target.dataset.attendee); + input.value = attendee["bbbg.attendee/meetup-name"]; + attendeeIDInput.value = attendee["bbbg.attendee/id"]; + + submit.classList.remove("hidden"); + submit.removeAttribute("disabled"); + signupForm.removeAttribute("disabled"); + }); + } + + document.querySelectorAll("form").forEach((form) => { + form.addEventListener("submit", (e) => { + if (e.target.attributes.disabled) { + e.preventDefault(); + } + + const confirmMessage = e.target.dataset.confirm; + if (confirmMessage != null && !confirm(confirmMessage)) { + e.stopImmediatePropagation(); + e.preventDefault(); + } + }); + }); +}; diff --git a/users/aspen/bbbg/resources/public/robots.txt b/users/aspen/bbbg/resources/public/robots.txt new file mode 100644 index 000000000000..1f53798bb4fe --- /dev/null +++ b/users/aspen/bbbg/resources/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/users/aspen/bbbg/shell.nix b/users/aspen/bbbg/shell.nix new file mode 100644 index 000000000000..c253a2b9bad2 --- /dev/null +++ b/users/aspen/bbbg/shell.nix @@ -0,0 +1,29 @@ +let + depot = import ../../.. { }; +in +with depot.third_party.nixpkgs; + +mkShell { + buildInputs = [ + arion + depot.third_party.clj2nix + clojure + openjdk11_headless + postgresql_12 + nix-prefetch-git + (writeShellScriptBin "terraform" '' + set -e + module=$(nix-build ~/code/depot -A users.grfn.bbbg.tf.module) + rm -f ~/tfstate/bbbg/*.json + cp ''${module}/*.json ~/tfstate/bbbg + exec ${depot.users.aspen.bbbg.tf.terraform}/bin/terraform \ + -chdir=/home/grfn/tfstate/bbbg \ + "$@" + '') + ]; + + PGHOST = "localhost"; + PGUSER = "bbbg"; + PGDATABASE = "bbbg"; + PGPASSWORD = "password"; +} diff --git a/users/aspen/bbbg/src/bbbg/attendee.clj b/users/aspen/bbbg/src/bbbg/attendee.clj new file mode 100644 index 000000000000..49a6d621de66 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/attendee.clj @@ -0,0 +1,10 @@ +(ns bbbg.attendee + (:require [clojure.spec.alpha :as s])) + +(s/def ::id uuid?) + +(s/def ::meetup-name (s/and string? seq)) + +(s/def ::discord-name (s/nilable string?)) + +(s/def ::organizer-notes string?) diff --git a/users/aspen/bbbg/src/bbbg/attendee_check.clj b/users/aspen/bbbg/src/bbbg/attendee_check.clj new file mode 100644 index 000000000000..f34c41198e66 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/attendee_check.clj @@ -0,0 +1,4 @@ +(ns bbbg.attendee-check + (:require [clojure.spec.alpha :as s])) + +(s/def ::id uuid?) diff --git a/users/aspen/bbbg/src/bbbg/core.clj b/users/aspen/bbbg/src/bbbg/core.clj new file mode 100644 index 000000000000..632774d5cdac --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/core.clj @@ -0,0 +1,69 @@ +(ns bbbg.core + (:gen-class) + (:require + [bbbg.db :as db] + [bbbg.web :as web] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as stest] + [com.stuartsierra.component :as component] + [expound.alpha :as exp])) + +(s/def ::config + (s/merge + ::db/config + ::web/config)) + +(defn make-system [config] + (component/system-map + :db (db/make-database config) + :web (web/make-server config))) + +(defn env->config [] + (s/assert + ::config + (merge + (db/env->config) + (web/env->config)))) + +(defn dev-config [] + (s/assert + ::config + (merge + (db/dev-config) + (web/dev-config)))) + +(defonce system nil) + +(defn init-dev [] + (s/check-asserts true) + (set! s/*explain-out* exp/printer) + (stest/instrument)) + +(defn run-dev [] + (init-dev) + (alter-var-root + #'system + (fn [sys] + (when sys + (component/start sys)) + (component/start (make-system (dev-config)))))) + +(defn -main [& _args] + (alter-var-root + #'system + (constantly (component/start (make-system (env->config)))))) + +(comment + ;; To run the application: + ;; 1. `M-x cider-jack-in` + ;; 2. `M-x cider-load-buffer` in this buffer + ;; 3. (optionally) configure the secrets backend in `bbbg.util.dev-secrets` + ;; 4. Put your cursor after the following form and run `M-x cider-eval-last-sexp` + ;; + ;; A web server will be listening on http://localhost:8888 + + (do + (run-dev) + (bbbg.db/migrate! (:db system))) + + ) diff --git a/users/aspen/bbbg/src/bbbg/db.clj b/users/aspen/bbbg/src/bbbg/db.clj new file mode 100644 index 000000000000..5bbf88925aa1 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/db.clj @@ -0,0 +1,366 @@ +(ns bbbg.db + (:gen-class) + (:refer-clojure :exclude [get list count]) + (:require [camel-snake-kebab.core :as csk :refer [->kebab-case ->snake_case]] + [bbbg.util.core :as u] + [clojure.set :as set] + [clojure.spec.alpha :as s] + [clojure.string :as str] + [com.stuartsierra.component :as component] + [config.core :refer [env]] + [honeysql.format :as hformat] + [migratus.core :as migratus] + [next.jdbc :as jdbc] + [next.jdbc.connection :as jdbc.conn] + next.jdbc.date-time + [next.jdbc.optional :as jdbc.opt] + [next.jdbc.result-set :as rs] + [next.jdbc.sql :as sql]) + (:import [com.impossibl.postgres.jdbc PGSQLSimpleException] + com.zaxxer.hikari.HikariDataSource + [java.sql Connection ResultSet Types] + javax.sql.DataSource)) + +(s/def ::host string?) +(s/def ::database string?) +(s/def ::user string?) +(s/def ::password string?) + +(s/def ::config + (s/keys :opt [::host + ::database + ::user + ::password])) + +(s/fdef make-database + :args + (s/cat :config (s/keys :opt [::config]))) + +(s/fdef env->config :ret ::config) + +(s/def ::db any?) + +;;; + +(def default-config + (s/assert + ::config + {::host "localhost" + ::database "bbbg" + ::user "bbbg" + ::password "password"})) + +(defn dev-config [] default-config) + +(defn env->config [] + (->> + {::host (:pghost env) + ::database (:pgdatabase env) + ::user (:pguser env) + ::password (:pgpassword env)} + u/remove-nils + (s/assert ::config))) + +(defn ->db-spec [config] + (-> default-config + (merge config) + (set/rename-keys + {::host :host + ::database :dbname + ::user :username + ::password :password}) + (assoc :dbtype "pgsql"))) + +(defn connection + "Make a one-off connection from the given `::config` map, or the environment + if not provided" + ([] (connection (env->config))) + ([config] + (-> config + ->db-spec + (set/rename-keys {:username :user}) + jdbc/get-datasource + jdbc/get-connection))) + +(defrecord Database [config] + component/Lifecycle + (start [this] + (assoc this :pool (jdbc.conn/->pool HikariDataSource (->db-spec config)))) + (stop [this] + (some-> this :pool .close) + (dissoc this :pool)) + + clojure.lang.IFn + (invoke [this] (:pool this))) + +(defn make-database [config] + (map->Database {:config config})) + +(defn database? [x] + (or + (instance? Database x) + (and (map? x) (contains? x :pool)))) + +;;; +;;; Migrations +;;; + +(defn migratus-config + [db] + {:store :database + :migration-dir "migrations/" + :migration-table-name "__migrations__" + :db + (let [db (if (ifn? db) (db) db)] + (cond + (.isInstance Connection db) + {:connection db} + (.isInstance DataSource db) + {:datasource db} + :else (throw + (ex-info "migratus-config called with value of unrecognized type" + {:value db}))))}) + +(defn generate-migration + ([db name] (generate-migration db name :sql)) + ([db name type] (migratus/create (migratus-config db) name type))) + +(defn migrate! + [db] (migratus/migrate (migratus-config db))) + +(defn rollback! + [db] (migratus/rollback (migratus-config db))) + +;;; +;;; Database interaction +;;; + +(defn ->key-ns [tn] + (let [tn (name tn) + tn (if (str/starts-with? tn "public.") + (second (str/split tn #"\." 2)) + tn)] + (str "bbbg." (->kebab-case tn)))) + +(defn ->table-name [kns] + (let [kns (name kns)] + (->snake_case + (if (str/starts-with? kns "public.") + kns + (str "public." (last (str/split kns #"\."))))))) + +(defn ->column + ([col] (->column nil col)) + ([table col] + (let [col-table (some-> col namespace ->table-name) + snake-col (-> col name ->snake_case (str/replace #"\?$" ""))] + (if (or (not (namespace col)) + (not table) + (= (->table-name table) col-table)) + snake-col + ;; different table, assume fk + (str + (str/replace-first col-table "public." "") + "_" + snake-col))))) + +(defn ->value [v] + (if (keyword? v) + (-> v name csk/->snake_case_string) + v)) + +(defn process-key-map [table key-map] + (into {} + (map (fn [[k v]] [(->column table k) + (->value v)])) + key-map)) + +(defn fkize [col] + (if (str/ends-with? col "-id") + (let [table (str/join "-" (butlast (str/split (name col) #"-")))] + (keyword (->key-ns table) "id")) + col)) + +(def ^:private enum-members-cache (atom {})) +(defn- enum-members + "Returns a set of enum members as strings for the enum with the given name" + [db name] + (if-let [e (find @enum-members-cache name)] + (val e) + (let [r (try + (-> (jdbc/execute-one! + (db) + [(format "select enum_range(null::%s) as members" name)]) + :members + .getArray + set) + (catch PGSQLSimpleException _ + nil))] + (swap! enum-members-cache assoc name r) + r))) + +(def ^{:private true + :dynamic true} + *meta-db* + "Database connection to use to query metadata" + nil) + +(extend-protocol rs/ReadableColumn + String + (read-column-by-label [x _] x) + (read-column-by-index [x rsmeta idx] + (if-not *meta-db* + x + (let [typ (.getColumnTypeName rsmeta idx)] + ;; TODO: Is there a better way to figure out if a type is an enum? + (if (enum-members *meta-db* typ) + (keyword (csk/->kebab-case-string typ) + (csk/->kebab-case-string x)) + x))))) + +(comment + (->key-ns :public.user) + (->key-ns :public.api-token) + (->key-ns :api-token) + (->table-name :api-token) + (->table-name :public.user) + (->table-name :bbbg.user) + ) + +(defn as-fq-maps [^ResultSet rs _opts] + (let [qualify #(when (seq %) (str "bbbg." (->kebab-case %))) + rsmeta (.getMetaData rs) + cols (mapv + (fn [^Integer i] + (let [ty (.getColumnType rsmeta i) + lab (.getColumnLabel rsmeta i) + n (str (->kebab-case lab) + (when (= ty Types/BOOLEAN) "?"))] + (fkize + (if-let [q (some-> rsmeta (.getTableName i) qualify not-empty)] + (keyword q n) + (keyword n))))) + (range 1 (inc (.getColumnCount rsmeta))))] + (jdbc.opt/->MapResultSetOptionalBuilder rs rsmeta cols))) + +(def jdbc-opts + {:builder-fn as-fq-maps + :column-fn ->snake_case + :table-fn ->snake_case}) + +(defmethod hformat/fn-handler "count-distinct" [_ field] + (str "count(distinct " (hformat/to-sql field) ")")) + +(defn fetch + "Fetch a single row from the db matching the given `sql-map` or query" + [db sql-map & [opts]] + (s/assert + (s/nilable (s/keys)) + (binding [*meta-db* db] + (jdbc/execute-one! + (db) + (if (map? sql-map) + (hformat/format sql-map) + sql-map) + (merge jdbc-opts opts))))) + +(defn get + "Retrieve a single record from the given table by ID" + [db table id & [opts]] + (when id + (fetch + db + {:select [:*] + :from [table] + :where [:= :id id]} + opts))) + +(defn list + "Returns a list of rows from the db matching the given sql-map, table or + query" + [db sql-map-or-table & [opts]] + (s/assert + (s/coll-of (s/keys)) + (binding [*meta-db* db] + (jdbc/execute! + (db) + (cond + (map? sql-map-or-table) + (hformat/format sql-map-or-table) + (keyword? sql-map-or-table) + (hformat/format {:select [:*] :from [sql-map-or-table]}) + :else + sql-map-or-table) + (merge jdbc-opts opts))))) + +(defn count + [db sql-map] + (binding [*meta-db* db] + (:count + (fetch db {:select [[:%count.* :count]], :from [[sql-map :sq]]})))) + +(defn exists? + "Returns true if the given sql query-map would return any results" + [db sql-map] + (binding [*meta-db* db] + (pos? + (count db sql-map)))) + +(defn execute! + "Given a database and a honeysql query map, perform an operation on the + database and discard the results" + [db sql-map & [opts]] + (jdbc/execute! + (db) + (hformat/format sql-map) + (merge jdbc-opts opts))) + +(defn insert! + "Given a database, a table name, and a data hash map, inserts the + data as a single row in the database and attempts to return a map of generated + keys." + [db table key-map & [opts]] + (binding [*meta-db* db] + (sql/insert! + (db) + table + (process-key-map table key-map) + (merge jdbc-opts opts)))) + +(defn update! + "Given a database, a table name, a hash map of columns and values + to set, and a honeysql predicate, perform an update on the table. + Will " + [db table key-map where-params & [opts]] + (binding [*meta-db* db] + (execute! db + {:update table + :set (u/map-keys keyword (process-key-map table key-map)) + :where where-params + :returning [:id]} + opts))) + +(defn delete! + "Delete all rows from the given table matching the given where clause" + [db table where-clause] + (binding [*meta-db* db] + (sql/delete! (db) table (hformat/format-predicate where-clause)))) + +(defmacro with-transaction [[sym db opts] & body] + `(jdbc/with-transaction + [tx# (~db) ~opts] + (let [~sym (constantly tx#)] + ~@body))) + +(defn -main [& args] + (let [db (component/start (make-database (env->config)))] + (case (first args) + "migrate" (migrate! db) + "rollback" (rollback! db)))) + +(comment + (def db (:db bbbg.core/system)) + (generate-migration db "add-attendee-unique-meetup-id") + (migrate! db) + + ) diff --git a/users/aspen/bbbg/src/bbbg/db/attendee.clj b/users/aspen/bbbg/src/bbbg/db/attendee.clj new file mode 100644 index 000000000000..da5ee29321fb --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/db/attendee.clj @@ -0,0 +1,85 @@ +(ns bbbg.db.attendee + (:require + [bbbg.attendee :as attendee] + [bbbg.db :as db] + [bbbg.util.sql :refer [count-where]] + honeysql-postgres.helpers + [honeysql.helpers + :refer + [merge-group-by merge-join merge-left-join merge-select merge-where]] + [bbbg.util.core :as u])) + +(defn search + ([q] (search {:select [:attendee.*] :from [:attendee]} q)) + ([db-or-query q] + (if (db/database? db-or-query) + (db/list db-or-query (search q)) + (cond-> db-or-query + q (merge-where + [:or + [:ilike :meetup_name (str "%" q "%")] + [:ilike :discord_name (str "%" q "%")]])))) + ([db query q] + (db/list db (search query q)))) + +(defn for-event + ([event-id] + (for-event {:select [:attendee.*] + :from [:attendee]} + event-id)) + ([db-or-query event-id] + (if (db/database? db-or-query) + (db/list db-or-query (for-event event-id)) + (-> db-or-query + (merge-select :event-attendee.*) + (merge-join :event_attendee [:= :attendee.id :event_attendee.attendee_id]) + (merge-where [:= :event_attendee.event_id event-id])))) + ([db query event-id] + (db/list db (for-event query event-id)))) + +(defn with-stats + ([] (with-stats {:select [:attendee.*] + :from [:attendee]})) + ([query] + (-> query + (merge-left-join :event_attendee [:= :attendee.id :event_attendee.attendee_id]) + (merge-group-by :attendee.id) + (merge-select + [(count-where :event_attendee.rsvpd_attending) :events-rsvpd] + [(count-where :event_attendee.attended) :events-attended] + [(count-where [:and + :event_attendee.rsvpd_attending + [:not :event_attendee.attended]]) + :no-shows])))) + +(defn upsert-all! + [db attendees] + (when (seq attendees) + (db/list + db + {:insert-into :attendee + :values (map #(->> % + (db/process-key-map :attendee) + (u/map-keys keyword)) + attendees) + :upsert {:on-conflict [:meetup-user-id] + :do-update-set [:meetup-name]} + :returning [:id :meetup-user-id]}))) + +(comment + (def db (:db bbbg.core/system)) + (db/database? db) + (search db "gri") + (db/insert! db :attendee {::attendee/meetup-name "Griffin Smith" + ::attendee/discord-name "grfn" + }) + + (search db (with-stats) "gri") + + (search (with-stats) "gri") + + (db/list db (with-stats)) + + (db/insert! db :attendee {::attendee/meetup-name "Rando Guy" + ::attendee/discord-name "rando"}) + ) diff --git a/users/aspen/bbbg/src/bbbg/db/attendee_check.clj b/users/aspen/bbbg/src/bbbg/db/attendee_check.clj new file mode 100644 index 000000000000..492f786bd660 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/db/attendee_check.clj @@ -0,0 +1,55 @@ +(ns bbbg.db.attendee-check + (:require + [bbbg.attendee :as attendee] + [bbbg.attendee-check :as attendee-check] + [bbbg.db :as db] + [bbbg.user :as user] + [bbbg.util.core :as u])) + +(defn create! [db params] + (db/insert! db :attendee-check + (select-keys params [::attendee/id + ::user/id + ::attendee-check/last-dose-at]))) + +(defn attendees-with-last-checks + [db attendees] + (when (seq attendees) + (let [ids (map ::attendee/id attendees) + checks + (db/list db {:select [:attendee-check.*] + :from [:attendee-check] + :join [[{:select [:%max.attendee-check.checked-at + :attendee-check.attendee-id] + :from [:attendee-check] + :group-by [:attendee-check.attendee-id] + :where [:in :attendee-check.attendee-id ids]} + :last-check] + [:= + :attendee-check.attendee-id + :last-check.attendee-id]]}) + users (if (seq checks) + (u/key-by + ::user/id + (db/list db {:select [:public.user.*] + :from [:public.user] + :where [:in :id (map ::user/id checks)]})) + {}) + checks (map #(assoc % :user (users (::user/id %))) checks) + attendee-id->check (u/key-by ::attendee/id checks)] + (map #(assoc % :last-check (attendee-id->check (::attendee/id %))) + attendees)))) + +(comment + (def db (:db bbbg.core/system)) + + (attendees-with-last-checks + db + (db/list db :attendee) + ) + + (db/insert! db :attendee-check + {::attendee/id #uuid "58bcd372-ff6e-49df-b280-23d24c5ba0f0" + ::user/id #uuid "303fb606-5ef0-4682-ad7d-6429c670cd78" + ::attendee-check/last-dose-at "2021-12-19"}) + ) diff --git a/users/aspen/bbbg/src/bbbg/db/event.clj b/users/aspen/bbbg/src/bbbg/db/event.clj new file mode 100644 index 000000000000..1b5a4e11ecd7 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/db/event.clj @@ -0,0 +1,94 @@ +(ns bbbg.db.event + (:require + [bbbg.attendee :as attendee] + [bbbg.db :as db] + [bbbg.event :as event] + [bbbg.util.sql :refer [count-where]] + [honeysql.helpers + :refer [merge-group-by merge-left-join merge-select merge-where]] + [java-time :refer [local-date local-date-time local-time]])) + +(defn create! [db event] + (db/insert! db :event (select-keys event [::event/date]))) + +(defn attended! + [db params] + (db/execute! + db + {:insert-into :event-attendee + :values [{:event_id (::event/id params) + :attendee_id (::attendee/id params) + :attended true}] + :upsert {:on-conflict [:event-id :attendee-id] + :do-update-set! {:attended true}}})) + +(defn on-day + ([day] {:select [:event.*] + :from [:event] + :where [:= :date (str day)]}) + ([db day] + (db/list db (on-day day)))) + + +(def end-of-day-hour + ;; 7am utc = 3am nyc + 7) + +(defn current-day + ([] (current-day (local-date-time))) + ([dt] + (if (<= 0 + (.getHour (local-time dt)) + end-of-day-hour) + (java-time/minus + (local-date dt) + (java-time/days 1)) + (local-date dt)))) + +(comment + (current-day + (local-date-time + 2022 5 1 + 1 13 0)) + ) + +(defn today + ([] (on-day (current-day))) + ([db] (db/list db (today)))) + +(defn upcoming + ([] (upcoming {:select [:event.*] :from [:event]})) + ([query] + (merge-where query [:>= :date (local-date)]))) + +(defn past + ([] (past {:select [:event.*] :from [:event]})) + ([query] + (merge-where query [:< :date (local-date)]))) + +(defn with-attendee-counts + [query] + (-> query + (merge-left-join :event_attendee [:= :event.id :event_attendee.event-id]) + (merge-select :%count.event_attendee.attendee_id) + (merge-group-by :event.id :event_attendee.event-id))) + +(defn with-stats + [query] + (-> query + (merge-left-join :event_attendee [:= :event.id :event_attendee.event-id]) + (merge-select + [(count-where :event-attendee.rsvpd_attending) :num-rsvps] + [(count-where :event-attendee.attended) :num-attendees]) + (merge-group-by :event.id))) + +(comment + (def db (:db bbbg.core/system)) + (db/list db (-> (today) (with-attendee-counts))) + + (honeysql.format/format + (honeysql-postgres.helpers/upsert {:insert-into :foo + :values {:bar 1}} + (-> (honeysql-postgres.helpers/on-conflict :did) + (honeysql-postgres.helpers/do-update-set! [:did true])))) + ) diff --git a/users/aspen/bbbg/src/bbbg/db/event_attendee.clj b/users/aspen/bbbg/src/bbbg/db/event_attendee.clj new file mode 100644 index 000000000000..31411e5d4504 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/db/event_attendee.clj @@ -0,0 +1,17 @@ +(ns bbbg.db.event-attendee + (:require honeysql-postgres.format + [bbbg.db :as db] + [bbbg.util.core :as u])) + +(defn upsert-all! + [db attendees] + (when (seq attendees) + (db/execute! + db + {:insert-into :event-attendee + :values (map #(->> % + (db/process-key-map :event-attendee) + (u/map-keys keyword)) + attendees) + :upsert {:on-conflict [:event-id :attendee-id] + :do-update-set [:rsvpd-attending]}}))) diff --git a/users/aspen/bbbg/src/bbbg/db/user.clj b/users/aspen/bbbg/src/bbbg/db/user.clj new file mode 100644 index 000000000000..700105ef6350 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/db/user.clj @@ -0,0 +1,19 @@ +(ns bbbg.db.user + (:require [bbbg.db :as db] + [bbbg.user :as user])) + +(defn create! [db attrs] + (db/insert! db + :public.user + (select-keys attrs [::user/id + ::user/username + ::user/discord-user-id]))) + +(defn find-or-create! [db attrs] + (or + (db/fetch db {:select [:*] + :from [:public.user] + :where [:= + :discord-user-id + (::user/discord-user-id attrs)]}) + (create! db attrs))) diff --git a/users/aspen/bbbg/src/bbbg/discord.clj b/users/aspen/bbbg/src/bbbg/discord.clj new file mode 100644 index 000000000000..e854ec1d147d --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/discord.clj @@ -0,0 +1,44 @@ +(ns bbbg.discord + (:refer-clojure :exclude [get]) + (:require + [bbbg.util.dev-secrets :refer [secret]] + [clj-http.client :as http] + [clojure.string :as str])) + +(def base-uri "https://discord.com/api") + +(defn api-uri [path] + (str base-uri + (when-not (str/starts-with? path "/") "/") + path)) + +(defn get + ([token path] + (get token path {})) + ([token path params] + (:body + (http/get (api-uri path) + (-> params + (assoc :accept :json + :as :json) + (assoc-in [:headers "authorization"] + (str "Bearer " (:token token)))))))) + +(defn me [token] + (get token "/users/@me")) + +(defn guilds [token] + (get token "/users/@me/guilds")) + +(defn guild-member [token guild-id] + (get token (str "/users/@me/guilds/" guild-id "/member"))) + +(comment + (def token {:token (secret "bbbg/test-token")}) + (me token) + (guilds token) + (guild-member token "841295283564052510") + + (get token "/guilds/841295283564052510/roles") + + ) diff --git a/users/aspen/bbbg/src/bbbg/discord/auth.clj b/users/aspen/bbbg/src/bbbg/discord/auth.clj new file mode 100644 index 000000000000..35bc580e3933 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/discord/auth.clj @@ -0,0 +1,90 @@ +(ns bbbg.discord.auth + (:require + [bbbg.discord :as discord] + [bbbg.util.core :as u] + [bbbg.util.dev-secrets :refer [secret]] + clj-time.coerce + [clojure.spec.alpha :as s] + [config.core :refer [env]] + [ring.middleware.oauth2 :refer [wrap-oauth2]])) + +(s/def ::client-id string?) +(s/def ::client-secret string?) +(s/def ::bbbg-guild-id string?) +(s/def ::bbbg-organizer-role string?) + +(s/def ::config (s/keys :req [::client-id + ::client-secret + ::bbbg-guild-id + ::bbbg-organizer-role])) + +;;; + +(defn env->config [] + (s/assert + ::config + {::client-id (:discord-client-id env) + ::client-secret (:discord-client-secret env) + ::bbbg-guild-id (:bbbg-guild-id env "841295283564052510") + ::bbbg-organizer-role (:bbbg-organizer-role + env + ;; TODO this might not be the right id + "908428000817725470")})) + +(defn dev-config [] + (s/assert + ::config + {::client-id (secret "bbbg/discord-client-id") + ::client-secret (secret "bbbg/discord-client-secret") + ::bbbg-guild-id "841295283564052510" + ::bbbg-organizer-role "908428000817725470"})) + +;;; + +(def access-token-url + "https://discord.com/api/oauth2/token") + +(def authorization-url + "https://discord.com/api/oauth2/authorize") + +(def revoke-url + "https://discord.com/api/oauth2/token/revoke") + +(def scopes ["guilds" + "guilds.members.read" + "identify"]) + +(defn discord-oauth-profile [{:keys [base-url] :as env}] + {:authorize-uri authorization-url + :access-token-uri access-token-url + :client-id (::client-id env) + :client-secret (::client-secret env) + :scopes scopes + :launch-uri "/auth/discord" + :redirect-uri (str base-url "/auth/discord/redirect") + :landing-uri (str base-url "/auth/success")}) + +(comment + (-> "https://bbbg-staging.gws.fyi/auth/login" + (java.net.URI/create) + (.resolve "https://bbbg.gws.fyi/auth/discord/redirect") + str) + ) + +(defn wrap-discord-auth [handler env] + (wrap-oauth2 handler {:discord (discord-oauth-profile env)})) + +(defn check-discord-auth + "Check that the user with the given token has the correct level of discord + auth" + [{::keys [bbbg-guild-id bbbg-organizer-role]} token] + (and (some (comp #{bbbg-guild-id} :id) + (discord/guilds token)) + (some #{bbbg-organizer-role} + (:roles (discord/guild-member token bbbg-guild-id))))) + +(comment + (#'ring.middleware.oauth2/valid-profile? + (discord-oauth-profile + (dev-config))) + ) diff --git a/users/aspen/bbbg/src/bbbg/event.clj b/users/aspen/bbbg/src/bbbg/event.clj new file mode 100644 index 000000000000..aa0578f3546b --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/event.clj @@ -0,0 +1,4 @@ +(ns bbbg.event + (:require [clojure.spec.alpha :as s])) + +(s/def ::id uuid?) diff --git a/users/aspen/bbbg/src/bbbg/event_attendee.clj b/users/aspen/bbbg/src/bbbg/event_attendee.clj new file mode 100644 index 000000000000..7b6b4c27648b --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/event_attendee.clj @@ -0,0 +1,6 @@ +(ns bbbg.event-attendee + (:require [clojure.spec.alpha :as s])) + +(s/def ::attended? boolean?) + +(s/def ::rsvpd-attending? boolean?) diff --git a/users/aspen/bbbg/src/bbbg/handlers/attendee_checks.clj b/users/aspen/bbbg/src/bbbg/handlers/attendee_checks.clj new file mode 100644 index 000000000000..d7307c40673b --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/attendee_checks.clj @@ -0,0 +1,68 @@ +(ns bbbg.handlers.attendee-checks + (:require + [bbbg.attendee :as attendee] + [bbbg.attendee-check :as attendee-check] + [bbbg.db :as db] + [bbbg.db.attendee-check :as db.attendee-check] + [bbbg.handlers.core :refer [page-response wrap-auth-required]] + [bbbg.user :as user] + [bbbg.util.display :refer [format-date]] + [compojure.coercions :refer [as-uuid]] + [compojure.core :refer [context GET POST]] + [ring.util.response :refer [not-found redirect]] + [bbbg.views.flash :as flash])) + +(defn- edit-attendee-checks-page [{:keys [existing-check] + attendee-id ::attendee/id}] + [:div.page + (when existing-check + [:p + "Already checked on " + (-> existing-check ::attendee-check/checked-at format-date) + " by " + (::user/username existing-check)]) + [:form.attendee-checks-form + {:method :post + :action (str "/attendees/" attendee-id "/checks")} + [:div.form-group + [:label + "Last Dose" + [:input {:type :date + :name :last-dose-at}]]] + [:div.form-group + [:input {:type :submit + :value "Mark Checked"}]]]]) + +(defn attendee-checks-routes [{:keys [db]}] + (wrap-auth-required + (context "/attendees/:attendee-id/checks" [attendee-id :<< as-uuid] + (GET "/edit" [] + (if (db/exists? db {:select [1] + :from [:attendee] + :where [:= :id attendee-id]}) + (let [existing-check (db/fetch + db + {:select [:attendee-check.* + :public.user.*] + :from [:attendee-check] + :join [:public.user + [:= + :attendee-check.user-id + :public.user.id]] + :where [:= :attendee-id attendee-id]})] + (page-response + (edit-attendee-checks-page + {:existing-check existing-check + ::attendee/id attendee-id}))) + (not-found "Attendee not found"))) + (POST "/" {{:keys [last-dose-at]} :params + {user-id ::user/id} :session} + (db.attendee-check/create! + db + {::attendee/id attendee-id + ::user/id user-id + ::attendee-check/last-dose-at last-dose-at}) + (-> (redirect "/attendees") + (flash/add-flash + #:flash{:type :success + :message "Successfully updated vaccination status"})))))) diff --git a/users/aspen/bbbg/src/bbbg/handlers/attendees.clj b/users/aspen/bbbg/src/bbbg/handlers/attendees.clj new file mode 100644 index 000000000000..ce84b88e97c1 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/attendees.clj @@ -0,0 +1,162 @@ +(ns bbbg.handlers.attendees + (:require + [bbbg.attendee :as attendee] + [bbbg.attendee-check :as attendee-check] + [bbbg.db :as db] + [bbbg.db.attendee :as db.attendee] + [bbbg.db.attendee-check :as db.attendee-check] + [bbbg.db.event :as db.event] + [bbbg.event :as event] + [bbbg.handlers.core :refer [page-response wrap-auth-required]] + [bbbg.user :as user] + [bbbg.util.display :refer [format-date]] + [bbbg.views.flash :as flash] + [cheshire.core :as json] + [compojure.coercions :refer [as-uuid]] + [compojure.core :refer [GET POST routes]] + [honeysql.helpers :refer [merge-where]] + [ring.util.response :refer [content-type not-found redirect response]]) + (:import + java.util.UUID)) + +(defn- attendees-page [{:keys [attendees q edit-notes]}] + [:div.page + [:form.search-form {:method :get :action "/attendees"} + [:input.search-input + {:type "search" + :name "q" + :value q + :title "Search Attendees"}] + [:input {:type "submit" + :value "Search Attendees"}]] + [:table.attendees + [:thead + [:tr + [:th "Meetup Name"] + [:th "Discord Name"] + [:th "Events RSVPd"] + [:th "Events Attended"] + [:th "No-Shows"] + [:th "Last Vaccination Check"] + [:th "Notes"]]] + [:tbody + (for [attendee (sort-by + (comp #{edit-notes} ::attendee/id) + (comp - compare) + attendees) + :let [id (::attendee/id attendee)]] + [:tr + [:td.attendee-name (::attendee/meetup-name attendee)] + [:td + [:label.mobile-label "Discord Name: "] + (or (not-empty (::attendee/discord-name attendee)) + "—")] + [:td + [:label.mobile-label "Events RSVPd: "] + (:events-rsvpd attendee)] + [:td + [:label.mobile-label "Events Attended: "] + (:events-attended attendee)] + [:td + [:label.mobile-label "No-shows: "] + (:no-shows attendee)] + [:td + [:label.mobile-label "Last Vaccination Check: "] + (if-let [last-check (:last-check attendee)] + (str "✔️ "(-> last-check + ::attendee-check/checked-at + format-date) + ", by " + (get-in last-check [:user ::user/username])) + (list + [:span {:title "Not Checked"} + "❌"] + " " + [:a {:href (str "/attendees/" id "/checks/edit")} + "Edit"] ))] + (if (= edit-notes id) + [:td + [:form.organizer-notes {:method :post + :action (str "/attendees/" id "/notes")} + [:div.form-group + [:input {:type :text :name "notes" + :value (::attendee/organizer-notes attendee) + :autofocus true}]] + [:div.form-group + [:input {:type "Submit" :value "Save Notes"}]]]] + [:td + [:p + (::attendee/organizer-notes attendee)] + [:p + [:a {:href (str "/attendees?edit-notes=" id)} + "Edit Notes"]]])])]]]) + +(defn attendees-routes [{:keys [db]}] + (routes + (wrap-auth-required + (routes + (GET "/attendees" [q edit-notes] + (let [attendees (db/list db (cond-> (db.attendee/with-stats) + q (db.attendee/search q))) + attendees (db.attendee-check/attendees-with-last-checks + db + attendees) + edit-notes (some-> edit-notes UUID/fromString)] + (page-response (attendees-page {:attendees attendees + :q q + :edit-notes edit-notes})))) + + (POST "/attendees/:id/notes" [id :<< as-uuid notes] + (if (seq (db/update! db + :attendee + {::attendee/organizer-notes notes} + [:= :id id])) + (-> (redirect "/attendees") + (flash/add-flash + #:flash{:type :success + :message "Notes updated successfully"})) + (not-found "Attendee not found"))))) + + (GET "/attendees.json" [q event_id attended] + (let [results + (db/list + db + (cond-> + (if q + (db.attendee/search q) + {:select [:attendee.*] :from [:attendee]}) + event_id (db.attendee/for-event event_id) + (some? attended) + (merge-where + (case attended + "true" :attended + "false" [:or [:= :attended nil] [:not :attended]]))))] + (-> {:results results} + json/generate-string + response + (content-type "application/json")))) + + (POST "/event_attendees" [event_id attendee_id] + (if (and (db/exists? db {:select [:id] :from [:event] :where [:= :id event_id]}) + (db/exists? db {:select [:id] :from [:attendee] :where [:= :id attendee_id]})) + (do + (db.event/attended! db {::event/id event_id + ::attendee/id attendee_id}) + (-> (redirect (str "/signup-forms/" event_id)) + (flash/add-flash + #:flash{:type :success + :message "Thank you for signing in! Enjoy the event."}))) + (response "Something went wrong"))))) + +(comment + (def db (:db bbbg.core/system)) + (db/list db :attendee) + (db/list db + (-> + (db.attendee/search "gr") + (db.attendee/for-event #uuid "9f4f3eae-3317-41a7-843c-81bcae52aebf"))) + (honeysql.format/format + (-> + (db.attendee/search "gr") + (db.attendee/for-event #uuid "9f4f3eae-3317-41a7-843c-81bcae52aebf"))) + ) diff --git a/users/aspen/bbbg/src/bbbg/handlers/core.clj b/users/aspen/bbbg/src/bbbg/handlers/core.clj new file mode 100644 index 000000000000..caa679ee873f --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/core.clj @@ -0,0 +1,91 @@ +(ns bbbg.handlers.core + (:require + [bbbg.user :as user] + [bbbg.views.flash :as flash] + [hiccup.core :refer [html]] + [ring.util.response :refer [content-type response]] + [clojure.string :as str])) + +(def ^:dynamic *authenticated?* false) + +(defn authenticated? [request] + (some? (get-in request [:session ::user/id]))) + +(defn wrap-auth-required [handler] + (fn [req] + (when (authenticated? req) + (handler req)))) + +(defn wrap-dynamic-auth [handler] + (fn [req] + (binding [*authenticated?* (authenticated? req)] + (handler req)))) + +(def ^:dynamic *current-uri*) + +(defn wrap-current-uri [handler] + (fn [req] + (binding [*current-uri* (:uri req)] + (handler req)))) + +(defn nav-item [href label] + (let [active? + (when *current-uri* + (str/starts-with? + *current-uri* + href))] + [:li {:class (when active? "active")} + [:a {:href href} + label]])) + +(defn global-nav [] + [:nav.global-nav + [:ul + (nav-item "/events" "Events") + (when *authenticated?* + (nav-item "/attendees" "Attendees")) + [:li.spacer] + [:li + (if *authenticated?* + [:form.link-form + {:method :post + :action "/auth/sign-out"} + [:input {:type "submit" + :value "Sign Out"}]] + [:a {:href "/auth/discord"} + "Sign In"])]]]) + +(defn render-page [opts & body] + (let [[{:keys [title]} body] + (if (map? opts) + [opts body] + [{} (concat [opts] body)])] + (html + [:html {:lang "en"} + [:head + [:meta {:charset "UTF-8"}] + [:meta {:name "viewport" + :content "width=device-width,initial-scale=1"}] + [:title (if title + (str title " - BBBG") + "BBBG")] + [:link {:rel "stylesheet" + :type "text/css" + :href "/main.css"}]] + [:body + [:div.content + (global-nav) + #_(flash/render-flash flash/test-flash) + (flash/render-flash) + body] + [:script {:src "/main.js"}]]]))) + +(defn page-response [& render-page-args] + (-> (apply render-page render-page-args) + response + (content-type "text/html"))) + +(comment + (render-page + [:h1 "hi"]) + ) diff --git a/users/aspen/bbbg/src/bbbg/handlers/events.clj b/users/aspen/bbbg/src/bbbg/handlers/events.clj new file mode 100644 index 000000000000..6f6d6f3585ae --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/events.clj @@ -0,0 +1,259 @@ +(ns bbbg.handlers.events + (:require + [bbbg.db :as db] + [bbbg.db.attendee :as db.attendee] + [bbbg.db.event :as db.event] + [bbbg.event :as event] + [bbbg.handlers.core :refer [*authenticated?* page-response]] + [bbbg.meetup.import :refer [import-attendees!]] + [bbbg.util.display :refer [format-date pluralize]] + [bbbg.util.time :as t] + [bbbg.views.flash :as flash] + [compojure.coercions :refer [as-uuid]] + [compojure.core :refer [context GET POST]] + [java-time :refer [local-date]] + [ring.util.response :refer [not-found redirect]] + [bbbg.attendee :as attendee] + [bbbg.event-attendee :as event-attendee] + [bbbg.db.attendee-check :as db.attendee-check] + [bbbg.attendee-check :as attendee-check] + [bbbg.user :as user]) + (:import + java.time.format.FormatStyle)) + +(defn- num-attendees [event] + (str + (:num-attendees event) + (if (= (t/->LocalDate (::event/date event)) + (local-date)) + " Signed In" + (str " Attendee" (when-not (= 1 (:num-attendees event)) "s"))))) + +(def index-type->label + {:upcoming "Upcoming" + :past "Past"}) +(def other-index-type + {:upcoming :past + :past :upcoming}) + +(defn events-index + [{:keys [events num-events type]}] + [:div.page + [:div.page-header + [:h1 + (pluralize + num-events + (str (index-type->label type) " Event"))] + [:a {:href (str "/events" + (when (= :upcoming type) + "/past"))} + "View " + (index-type->label (other-index-type type)) + " Events"]] + (when *authenticated?* + [:a.button {:href "/events/new"} + "Create New Event"]) + [:ul.events-list + (for [event (sort-by + ::event/date + (comp - compare) + events)] + [:li + [:p + [:a {:href (str "/events/" (::event/id event))} + (format-date (::event/date event) + FormatStyle/FULL)]] + [:p + (pluralize (:num-rsvps event) "RSVP") + ", " + (num-attendees event)]])]]) + +(defn- import-attendee-list-form-group [] + [:div.form-group + [:label "Import Attendee List" + [:br] + [:input {:type :file + :name :attendees}]]]) + +(defn import-attendees-form [event] + [:form {:method :post + :action (str "/events/" (::event/id event) "/attendees") + :enctype "multipart/form-data"} + (import-attendee-list-form-group) + [:div.form-group + [:input {:type :submit + :value "Import"}]]]) + +(defn event-page [{:keys [event attendees]}] + [:div.page + [:div.page-header + [:h1 (format-date (::event/date event) + FormatStyle/FULL)] + [:div.spacer] + [:a.button {:href (str "/signup-forms/" (::event/id event) )} + "Go to Signup Form"] + [:form#delete-event + {:method :post + :action (str "/events/" (::event/id event) "/delete") + :data-confirm "Are you sure you want to delete this event?"} + [:input.error {:type "submit" + :value "Delete Event"}]]] + [:div.stats + [:p (pluralize (:num-rsvps event) "RSVP")] + [:p (num-attendees event)]] + [:div + (import-attendees-form event)] + [:div + [:table.attendees + [:thead + [:th "Meetup Name"] + [:th "Discord Name"] + [:th "RSVP"] + [:th "Signed In"] + [:th "Last Vaccination Check"]] + [:tbody + (for [attendee (sort-by (juxt (comp not ::event-attendee/rsvpd-attending?) + (comp not ::event-attendee/attended?) + (comp some? :last-check) + ::attendee/meetup-name) + attendees)] + [:tr + [:td.attendee-name (::attendee/meetup-name attendee)] + [:td + [:label.mobile-label "Discord Name: "] + (or (not-empty (::attendee/discord-name attendee)) + "—")] + [:td + [:label.mobile-label "RSVP: "] + (if (::event-attendee/rsvpd-attending? attendee) + [:span {:title "Yes"} "✔️"] + [:span {:title "No"} "❌"])] + [:td + [:label.mobile-label "Signed In: "] + (if (::event-attendee/attended? attendee) + [:span {:title "Yes"} "✔️"] + [:span {:title "No"} "❌"])] + [:td + [:label.mobile-label "Last Vaccination Check: "] + (if-let [last-check (:last-check attendee)] + (str "✔️ "(-> last-check + ::attendee-check/checked-at + format-date) + ", by " + (get-in last-check [:user ::user/username])) + (list + [:span {:title "Not Checked"} + "❌"] + " " + [:a {:href (str "/attendees/" + (::attendee/id attendee) + "/checks/edit")} + "Edit"]))]])]]]]) + +(defn import-attendees-page [{:keys [event]}] + [:div.page + [:h1 "Import Attendees for " (format-date (::event/date event))] + (import-attendees-form event)]) + +(defn event-form + ([] (event-form {})) + ([event] + [:div.page + [:div.page-header + [:h1 "Create New Event"]] + [:form {:method "POST" + :action "/events" + :enctype "multipart/form-data"} + [:div.form-group + [:label "Date" + [:input {:type "date" + :id "date" + :name "date" + :value (str (::event/date event))}]]] + (import-attendee-list-form-group) + [:div.form-group + [:input {:type "submit" + :value "Create Event"}]]]])) + +(defn- events-list-handler [db query type] + (let [events (db/list db (db.event/with-stats query)) + num-events (db/count db query)] + (page-response + (events-index {:events events + :num-events num-events + :type type})))) + +(defn events-routes [{:keys [db]}] + (context "/events" [] + (GET "/" [] + (events-list-handler db (db.event/upcoming) :upcoming)) + + (GET "/past" [] + (events-list-handler db (db.event/past) :past)) + + (GET "/new" [date] + (page-response + {:title "New Event"} + (event-form {::event/date date}))) + + (POST "/" [date attendees] + (let [event (db.event/create! db {::event/date date}) + message + (if attendees + (let [num-attendees + (import-attendees! db + (::event/id event) + (:tempfile attendees))] + (format "Event created with %d attendees" + num-attendees)) + "Event created")] + (-> (str "/signup-forms/" (::event/id event)) + redirect + (flash/add-flash {:flash/type :success + :flash/message message})))) + + (context "/:id" [id :<< as-uuid] + (GET "/" [] + (if-let [event (db/fetch db + (-> {:select [:event.*] + :from [:event] + :where [:= :event.id id]} + (db.event/with-stats)))] + (let [attendees (db.attendee-check/attendees-with-last-checks + db + (db/list db (db.attendee/for-event id)))] + (page-response + (event-page {:event event + :attendees attendees}))) + (not-found "Event Not Found"))) + + (POST "/delete" [] + (db/delete! db :event_attendee [:= :event-id id]) + (db/delete! db :event [:= :id id]) + (-> (redirect "/events") + (flash/add-flash + #:flash {:type :success + :message "Successfully deleted event"}))) + + (GET "/attendees/import" [] + (if-let [event (db/get db :event id)] + (page-response + (import-attendees-page {:event event})) + (not-found "Event Not Found"))) + + (POST "/attendees" [attendees] + (let [num-imported (import-attendees! db id (:tempfile attendees))] + (-> (redirect (str "/events/" id)) + (flash/add-flash + #:flash{:type :success + :message (format "Successfully imported %d attendees" + num-imported)}))))))) + +(comment + (def db (:db bbbg.core/system)) + + (-> (db/list db :event) + first + ::event/date + format-date) + ) diff --git a/users/aspen/bbbg/src/bbbg/handlers/home.clj b/users/aspen/bbbg/src/bbbg/handlers/home.clj new file mode 100644 index 000000000000..17d48755365c --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/home.clj @@ -0,0 +1,52 @@ +(ns bbbg.handlers.home + (:require + [bbbg.db.user :as db.user] + [bbbg.discord.auth :as discord.auth] + [bbbg.handlers.core :refer [page-response authenticated?]] + [bbbg.user :as user] + [bbbg.views.flash :as flash] + [compojure.core :refer [GET POST routes]] + [ring.util.response :refer [redirect]] + [bbbg.discord :as discord])) + +(defn- home-page [] + [:div.home-page + [:a.signup-form-link {:href "/signup-forms"} + "Event Signup Form"]]) + +(defn auth-failure [] + [:div.auth-failure + [:p + "Sorry, only users with the Organizers role in discord can sign in"] + [:p + [:a {:href "/"} "Go Back"]]]) + +(defn home-routes [{:keys [db] :as env}] + (routes + (GET "/" [] (page-response (home-page))) + + (POST "/auth/sign-out" request + (if (authenticated? request) + (-> (redirect "/") + (update :session dissoc ::user/id) + (flash/add-flash + {:flash/message "Successfully Signed Out" + :flash/type :success})) + (redirect "/"))) + + (GET "/auth/success" request + (let [token (get-in request [:oauth2/access-tokens :discord])] + (if (discord.auth/check-discord-auth env token) + (let [discord-user (discord/me token) + user (db.user/find-or-create! + db + #::user{:username (:username discord-user) + :discord-user-id (:id discord-user)})] + (-> (redirect "/") + (assoc-in [:session ::user/id] (::user/id user)) + (flash/add-flash + {:flash/message "Successfully Signed In" + :flash/type :success}))) + (-> + (page-response (auth-failure)) + (assoc :status 401))))))) diff --git a/users/aspen/bbbg/src/bbbg/handlers/signup_form.clj b/users/aspen/bbbg/src/bbbg/handlers/signup_form.clj new file mode 100644 index 000000000000..ed1d7644f539 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/handlers/signup_form.clj @@ -0,0 +1,93 @@ +(ns bbbg.handlers.signup-form + (:require + [bbbg.attendee :as attendee] + [bbbg.db :as db] + [bbbg.db.attendee :as db.attendee] + [bbbg.db.event :as db.event] + [bbbg.event :as event] + [bbbg.handlers.core + :refer [*authenticated?* authenticated? page-response]] + [cheshire.core :as json] + [compojure.core :refer [context GET]] + [honeysql.helpers :refer [merge-where]] + [java-time :refer [local-date]] + [ring.util.response :refer [redirect]])) + +(defn no-events-page [{:keys [authenticated?]}] + [:div.page + [:p + "There are no events for today"] + (when authenticated? + [:p + [:a.button {:href (str "/events/new?date=" (str (local-date)))} + "Create New Event"]])]) + +(defn signup-page [{:keys [event attendees]}] + [:div.signup-page + [:form#signup-form + {:method "POST" + :action "/event_attendees" + :disabled "disabled"} + [:input#name-autocomplete + {:type "search" + :title "Name" + :name "name" + :spellcheck "false" + :autocorrect "off" + :autocomplete "off" + :autocapitalize "off" + :maxlength "2048"}] + [:input#attendee-id {:type "hidden" :name "attendee_id"}] + [:input#event-id {:type "hidden" :name "event_id" :value (::event/id event)}] + [:input#submit-button.hidden + {:type "submit" + :value "Sign In" + :disabled "disabled"}]] + [:ul#attendees-list + (if (seq attendees) + (for [attendee attendees] + [:li {:data-attendee (json/generate-string attendee) + :role "button"} + (::attendee/meetup-name attendee)]) + [:li.no-attendees + [:p + "Nobody has RSVPed to this event yet, or no attendee list has been + imported"] + (when *authenticated?* + [:p + [:a.button + {:href (str "/events/" + (::event/id event) + "/attendees/import")} + "Import Attendee List"]])])]]) + +(defn event-not-found [] + [:div.event-not-found + [:p "Event not found"] + [:p [:a {:href (str "/events/new")} "Create a new event"]]]) + +;;; + +(defn signup-form-routes [{:keys [db]}] + (context "/signup-forms" [] + (GET "/" request + (if-let [event (db/fetch db (db.event/today))] + (redirect (str "/signup-forms/" (::event/id event))) + (page-response (no-events-page + {:authenticated? (authenticated? request)})))) + + (GET "/:event-id" [event-id] + (if-let [event (db/get db :event event-id)] + (let [attendees (db/list db + (-> + (db.attendee/for-event event-id) + (merge-where + [:and + [:or + [:= :attended nil] + [:not :attended]] + :rsvpd_attending])))] + (page-response + (signup-page {:event event + :attendees attendees}))) + (event-not-found))))) diff --git a/users/aspen/bbbg/src/bbbg/meetup/import.clj b/users/aspen/bbbg/src/bbbg/meetup/import.clj new file mode 100644 index 000000000000..bbf86789768c --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/meetup/import.clj @@ -0,0 +1,125 @@ +(ns bbbg.meetup.import + (:require + [bbbg.attendee :as attendee] + [bbbg.db.attendee :as db.attendee] + [bbbg.db.event-attendee :as db.event-attendee] + [bbbg.event :as event] + [bbbg.event-attendee :as event-attendee] + [bbbg.meetup-user :as meetup-user] + [bbbg.util.core :as u] + [bbbg.util.spec :as u.s] + [clojure.data.csv :as csv] + [clojure.java.io :as io] + [clojure.spec.alpha :as s] + [clojure.string :as str] + [expound.alpha :as exp])) + +(def spreadsheet-column->key + {"Name" :name + "User ID" :user-id + "Title" :title + "Event Host" :event-host + "RSVP" :rsvp + "Guests" :guests + "RSVPed on" :rsvped-on + "Joined Group on" :joined-group-on + "URL of Member Profile" :member-profile-url}) + +(defn read-attendees [f] + (with-open [reader (io/reader f)] + (let [[headers & rows] (-> reader (csv/read-csv :separator \tab)) + keys (map spreadsheet-column->key headers)] + (doall + (->> rows + (map (partial zipmap keys)) + (map (partial u/filter-kv (fn [k _] (some? k)))) + (filter (partial some (comp seq val)))))))) + +;;; + +(s/def ::imported-attendee + (s/keys :req [::attendee/meetup-name + ::meetup-user/id])) + +(def key->attendee-col + {:name ::attendee/meetup-name + :user-id ::meetup-user/id}) + +(defn row-user-id->user-id [row-id] + (str/replace-first row-id "user " "")) + +(defn check-attendee [attendee] + () + (if (s/valid? ::imported-attendee attendee) + attendee + (throw (ex-info + (str "Invalid imported attendee\n" + (exp/expound-str ::imported-attendee attendee)) + (assoc (s/explain-data ::imported-attendee attendee) + ::s/failure + ::s/assertion-failed))))) + +(defn row->attendee [r] + (u.s/assert! + ::imported-attendee + (update (u/keep-keys key->attendee-col r) + ::meetup-user/id row-user-id->user-id))) + +;;; + +(s/def ::imported-event-attendee + (s/keys :req [::event-attendee/rsvpd-attending? + ::attendee/id + ::event/id])) + +(def key->event-attendee-col + {:rsvp ::event-attendee/rsvpd-attending?}) + +(defn row->event-attendee + [{event-id ::event/id :keys [meetup-id->attendee-id]} r] + (let [attendee-id (-> r :user-id row-user-id->user-id meetup-id->attendee-id)] + (u.s/assert! + ::imported-event-attendee + (-> (u/keep-keys key->event-attendee-col r) + (update ::event-attendee/rsvpd-attending? + (partial = "Yes")) + (assoc ::event/id event-id + ::attendee/id attendee-id))))) + +;;; + +(defn import-attendees! [db event-id f] + (let [rows (read-attendees f) + attendees (db.attendee/upsert-all! db (map row->attendee rows)) + meetup-id->attendee-id (into {} + (map (juxt ::meetup-user/id ::attendee/id)) + attendees)] + (db.event-attendee/upsert-all! + db + (map (partial row->event-attendee + {::event/id event-id + :meetup-id->attendee-id meetup-id->attendee-id}) + rows)) + (count rows))) + +;;; Spreadsheet columns: +;;; +;;; Name +;;; User ID +;;; Title +;;; Event Host +;;; RSVP +;;; Guests +;;; RSVPed on +;;; Joined Group on +;;; URL of Member Profile +;;; Have you been to one of our events before? Note, attendance at all events will require proof of vaccination until further notice. + +(comment + (def -filename- "/home/aspen/code/depot/users/aspen/bbbg/sample-data.tsv") + (def event-id #uuid "09f8fed6-7480-451b-89a2-bb4edaeae657") + + (read-attendees -filename-) + (import-attendees! (:db bbbg.core/system) event-id -filename-) + + ) diff --git a/users/aspen/bbbg/src/bbbg/meetup_user.clj b/users/aspen/bbbg/src/bbbg/meetup_user.clj new file mode 100644 index 000000000000..945d681c6f82 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/meetup_user.clj @@ -0,0 +1,6 @@ +(ns bbbg.meetup-user + (:require [clojure.spec.alpha :as s])) + +(s/def ::id + (s/nilable + (s/and string? seq))) diff --git a/users/aspen/bbbg/src/bbbg/styles.clj b/users/aspen/bbbg/src/bbbg/styles.clj new file mode 100644 index 000000000000..a860ae607626 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/styles.clj @@ -0,0 +1,407 @@ +;; -*- eval: (rainbow-mode) -*- +(ns bbbg.styles + (:require + [garden.color :as color] + [garden.compiler :refer [compile-css]] + [garden.def :refer [defstyles]] + [garden.selectors + :refer [& active attr= descendant focus hover nth-child]] + [garden.stylesheet :refer [at-media]] + [garden.units :refer [px]])) + +(def black "#342e37") + +(def silver "#f9fafb") + +(def gray "#aaa") + +(def gray-light "#ddd") + +(def purple "#837aff") + +(def red "#c42348") + +(def orange "#fa824c") + +(def yellow "#FACB0F") + +(def blue "#026fb1") + +(def green "#87E24B") + +(def contextual-colors + {:success green + :info blue + :warning yellow + :error red}) + +;;; + +(def content-width (px 1200)) +(def mobile-width (px 480)) + +(defn desktop [& rules] + (at-media + {:screen true + :min-width content-width} + [:& rules])) + +(defn mobile [& rules] + (at-media + {:screen true + :max-width mobile-width} + [:& rules])) + +(defn not-mobile [& rules] + (at-media + {:screen true + :min-width mobile-width} + [:& rules])) + + +;;; + +(defstyles global-nav + [:.global-nav + {:background-color silver} + + [:>ul + {:display :flex + :flex-direction :row + :list-style :none} + + (desktop + {:width content-width + :margin "0 auto"})] + + [:a (descendant :.link-form (attr= "type" "submit")) + {:padding "1rem 1.5rem" + :display :block + :color black + :text-decoration :none} + + [(& hover) + {:color blue}]] + + [:li.active + {:font-weight "bold" + :border-bottom [["1px" "solid" black]]}]] + + [:.spacer + {:flex 1}]) + +(def link-conditional-styles + (list + [(& hover) (& active) + {:text-decoration :underline}] + [(& active) + {:color purple}])) + +(defstyles link-form + [:form.link-form + {:margin 0} + [(attr= "type" "submit") + {:background "none" + :border "none" + :padding 0 + :color blue + :text-decoration :none + :cursor :pointer} + link-conditional-styles]]) + +(defstyles search-form + [:.search-form + {:display :flex + :flex-direction :row + :width "100%"} + + [:>*+* + {:margin-left "0.75rem"}] + + [:input + {:flex 1}] + + [(attr= "type" "submit") + {:flex 0}]]) + +(defstyles forms + (let [text-input-types + #{"date" + "datetime-local" + "email" + "month" + "number" + "password" + "search" + "tel" + "text" + "time" + "url" + "week"} + each-text-type (fn [& rules] + (into + [] + (concat + (map (comp & (partial attr= "type")) + text-input-types) + rules)))] + (each-text-type + {:width "100%" + :display "block" + :padding "0.6rem 0.75rem" + :border [["1px" "solid" gray-light]] + :border-radius "3px" + :box-shadow [["inset" 0 "1px" "5px" "rgba(0,0,0,0.075)"]] + :transition "border-color 150ms" + :background "none"} + [(& focus) + {:outline "none" + :border-color purple}])) + + [(attr= "type" "submit") :button :.button + {:background-color (color/lighten blue 30) + :padding "0.6rem 0.75rem" + :border-radius "3px" + :border [[(px 1) "solid" (color/lighten blue 30)]] + :cursor :pointer + :display :inline-block} + + [(& hover) + {:border-color blue + :text-decoration :none + :box-shadow [[0 "1px" "5px" "rgba(0,0,0,0.075)"]]} + [(:a &) + {:text-decoration :none}]] + + [(& active) + {:background-color blue + :color :white + :box-shadow :none} + [(& :a) + {:text-decoration :none}]] + + (for [[context color] contextual-colors] + [(& (keyword (str "." (name context)))) + {:background-color (color/lighten color 30) + :border-color (color/lighten color 30) + :color black} + + [(& hover) + {:border-color color}]])] + + [:label + {:font-weight 600 + :width "100%"} + + [:input + {:font-weight "initial" + :margin-top "0.3rem"}]] + + [:.form-group + {:display :flex + :margin-bottom "0.8rem" + :flex-direction :column} + + [(attr= "type" "submit") + {:text-align :right + :align-self :flex-end}]]) + +(defstyles tables + [:table + {:width "100%" + :border-collapse "collapse"}] + + [:th + {:text-align "left"}] + + [:td :th + {:padding "0.75rem 1rem" + :border-spacing 0 + :border "none"}] + + [:tr + {:border-spacing 0 + :border "none"} + [(& (nth-child :even)) + {:background-color silver}]]) + +(defstyles flash + [:.flash-messages + {:max-width "800px" + :margin "1rem auto"} + + (at-media + {:screen true + :max-width "800px"} + [:& + {:margin-left "1rem" + :margin-right "1rem"}])] + + [:.flash-message + {:padding "1rem 1.5rem" + :border "1px solid" + :margin-bottom "1rem"}] + + (for [[context color] contextual-colors] + [(& (keyword (str ".flash-" (name context)))) + {:border-color color + :background-color (color/lighten color 30) + :border-radius "3px"}])) + +(defstyles home-page + [:.home-page + {:display :flex + :flex 1 + :justify-content :center + :align-items :center} + [:.signup-form-link + {:display :block + :border [["1px" :solid blue]] + :border-radius "3px" + :color black + :font-size "2rem" + :background-color (color/lighten blue 50) + :margin-left "auto" + :margin-right "auto" + :padding "2rem"} + (desktop + {:padding "5rem" + :margin-left 0 + :margin-right 0}) + [(& hover) (& active) + {:text-decoration :none}] + [(& active) + {:background-color (color/lighten blue 30)}]]]) + +(defstyles signup-page + [:.signup-page + {:margin "1rem"} + (desktop + {:width content-width + :margin "1rem auto"})] + + [:#signup-form + {:display :flex + :flex-direction :row + :width "100%"} + + [:* + {:flex 1}] + + [:*+* + {:margin-left "1rem"}] + + [(attr= "type" "submit") + {:flex 0}]] + + [:#attendees-list + {:list-style "none" + :overflow-y "auto" + :height "calc(100vh - 8.32425rem)"} + + [:li + {:padding "0.75rem 1rem" + :margin "0.35rem 0" + :border-radius "3px" + :background-color silver}]] + + [:.no-attendees + {:text-align "center" + :margin-top "6rem"} + + [:.button + {:margin-top "0.5rem"}]] + + [:.hidden + {:display :none}]) + +(defstyles attendees + [:.attendee-checks-form + {:max-width "340px" + :margin-left "auto" + :margin-right "auto"}] + + [:.attendees + (mobile + {:display :block} + + [:thead {:display :none}] + [:tbody :tr :td + {:display :block}] + + [:tr + {:background-color silver + :padding "0.5rem 0.8rem" + :margin-bottom "1rem" + :border-radius "3px"}] + [:td {:padding "0.2rem 0"}] + + [:.attendee-name + {:font-weight "bold" + :margin-bottom "0.9rem"}]) + + (not-mobile + [:.mobile-label + {:display :none}])]) + +(defstyles events + [:.events-list + {:margin-top "1rem"} + + [:li + {:margin-bottom "1rem"}]]) + +(defstyles styles + forms + tables + global-nav + link-form + search-form + flash + home-page + signup-page + attendees + events + + [:body + {:color black}] + + [:.content + {:display :flex + :flex-direction :column + :height "100%" + :width "100%"}] + + [:.page + {:margin-top "1rem" + :margin-left "1rem" + :margin-right "1rem"} + + (desktop + {:width content-width + :margin-left "auto" + :margin-right "auto"})] + + [:.page-header + {:display :flex + :flex-wrap :wrap + :padding-bottom "0.7rem" + :margin-bottom "1rem" + :border-bottom [["1px" "solid" silver]] + :align-items :center} + + [:*+* + {:margin-left "0.5rem"}] + + [:form + {:margin-block-end 0}]] + + [(attr= "role" "button") + {:cursor :pointer}] + + [:a {:color blue + :text-decoration :none} + link-conditional-styles]) + +(def stylesheet + (compile-css styles)) diff --git a/users/aspen/bbbg/src/bbbg/user.clj b/users/aspen/bbbg/src/bbbg/user.clj new file mode 100644 index 000000000000..f48c8d73388e --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/user.clj @@ -0,0 +1,8 @@ +(ns bbbg.user + (:require [clojure.spec.alpha :as s])) + +(s/def ::id uuid?) + +(s/def ::discord-id string?) + +(s/def ::username string?) diff --git a/users/aspen/bbbg/src/bbbg/util/core.clj b/users/aspen/bbbg/src/bbbg/util/core.clj new file mode 100644 index 000000000000..d458aa5592d2 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/util/core.clj @@ -0,0 +1,138 @@ +(ns bbbg.util.core + (:require + [clojure.java.shell :refer [sh]] + [clojure.string :as str]) + (:import + java.util.UUID)) + +(defn remove-nils + "Remove all keys with nil values from m" + [m] + (let [!m (transient m)] + (doseq [[k v] m] + (when (nil? v) + (dissoc! !m k))) + (persistent! !m))) + + +(defn alongside + "Apply a pair of functions to the first and second element of a two element + vector, respectively. The two argument form partially applies, such that: + + ((alongside f g) xy) ≡ (alongside f g xy) + + This is equivalent to (***) in haskell's Control.Arrow" + ([f g] (partial alongside f g)) + ([f g [x y]] [(f x) (g y)])) + +(defn map-kv + "Map a pair of functions over the keys and values of a map, respectively. + Preserves metadata on the incoming map. + The two argument form returns a transducer that yields map-entries. + + (partial map-kv identity identity) ≡ identity" + ([kf vf] + (map (fn [[k v]] + ;; important to return a map-entry here so that callers down the road + ;; can use `key` or `val` + (first {(kf k) (vf v)})))) + ([kf vf m] + (into (empty m) (map-kv kf vf) m))) + +(defn filter-kv + "Returns a map containing the elements of m for which (f k v) returns logical + true. The one-argument form returns a transducer that yields map entries" + ([f] (filter (partial apply f))) + ([f m] + (into (empty m) (filter-kv f) m))) + +(defn map-keys + "Map f over the keys of m. Preserves metadata on the incoming map. The + one-argument form returns a transducer that yields map-entries." + ([f] (map-kv f identity)) + ([f m] (map-kv f identity m))) + +(defn keep-keys + "Map f over the keys of m, keeping only those entries for which f does not + return nil. Preserves metadata on the incoming map. The one-argument form + returns a transducer that yields map-entries." + ([f] (keep (fn [[k v]] (when-let [k' (f k)] + (first {k' v}))))) + ([f m] (into (empty m) (keep-keys f) m))) + +(defn map-vals + "Map f over the values of m. Preserves metadata on the incoming map. The + one-argument form returns a transducer that yields map-entries." + ([f] (map-kv identity f)) + ([f m] (map-kv identity f m))) + +(defn map-keys-recursive [f x] + (cond + (map? x) (map-kv f (partial map-keys-recursive f) x) + (sequential? x) (map (partial map-keys-recursive f) x) + :else x)) + +(defn denamespace [x] + (if (keyword? x) + (keyword (name x)) + (map-keys-recursive denamespace x))) + +(defn reverse-merge + "Like `clojure.core/merge`, except duplicate keys from maps earlier in the + argument list take precedence + + => (merge {:x 1} {:x 2}) + {:x 2} + + => (sut/reverse-merge {:x 1} {:x 2}) + {:x 1}" + [& ms] + (apply merge (reverse ms))) + +(defn invert-map + "Invert the keys and vals of m. Behavior with duplicate vals is undefined. + + => (sut/invert-map {:x 1 :y 2}) + {1 :x 2 :y}" + [m] + (into {} (map (comp vec reverse)) m)) + +(defn ->uuid + "Converts x to uuid, returning nil if x is nil or empty" + [x] + (cond + (not x) nil + (uuid? x) x + (and (string? x) (seq x)) + (UUID/fromString x))) + +(defn key-by + "Create a map from a seq obtaining keys via f + + => (sut/key-by :x [{:x 1} {:x 2 :y 3}]) + {1 {:x 1}, 2 {:x 2 :y 3}}" + [f l] + (into {} (map (juxt f identity)) l)) + +(defn distinct-by + "Like clojure.core/distinct, but can take a function f by which + distinctiveness is calculated" + [distinction-fn coll] + (let [step (fn step [xs seen] + (lazy-seq + ((fn [[f :as xs] seen] + (when-let [s (seq xs)] + (if (contains? seen (distinction-fn f)) + (recur (rest s) seen) + (cons f (step (rest s) (conj seen (distinction-fn f))))))) + xs seen)))] + (step coll #{}))) + +(defn pass [n] + (let [{:keys [exit out err]} (sh "pass" n)] + (if (= 0 exit) + (str/trim out) + (throw (Exception. + (format "`pass` command failed\nStandard output:%s\nStandard Error:%s" + out + err)))))) diff --git a/users/aspen/bbbg/src/bbbg/util/dev_secrets.clj b/users/aspen/bbbg/src/bbbg/util/dev_secrets.clj new file mode 100644 index 000000000000..88f1b50caaa8 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/util/dev_secrets.clj @@ -0,0 +1,59 @@ +(ns bbbg.util.dev-secrets + "Utility library for loading secrets during development from multiple + backends. + + # Supported backends + + - [Pass][0] (the default) + + (bbbg.util.dev-secrets/set-backend! :pass) + + Loads all secrets by shelling out to `pass <secret-name>` + + [0]: https://www.passwordstore.org/ + + - Directory + + (bbbg.util.dev-secrets/set-backend! [:dir \"/path/to/secret/directory\"]) + + Loads all secrets by reading the secret name as a (plaintext!) file rooted + at the given directory" + (:require [bbbg.util.core :as u] + [clojure.string :as str] + [clojure.java.io :as io])) + +(def ^:dynamic *secret-backend* :pass) + +(defn set-backend! + "Change the default secret-backend" + [backend] + (alter-var-root #'*secret-backend* (constantly backend))) + +(defmulti ^:private load-secret + (fn [backend _secret] + (if (coll? backend) (first backend) backend))) + +(defmethod load-secret :pass [_ secret] + (u/pass secret)) + +(defmethod load-secret :dir [[_ dir] secret] + (str/trim (slurp (io/file dir secret)))) + +(defn secret + "Load the value for the given `secret-name' from the currently selected + backend" + [secret-name] + (load-secret *secret-backend* secret-name)) + +(comment + (secret "bbbg/discord-client-id") + + (binding [*secret-backend* [:dir "/tmp/bbbg-secrets"]] + (secret "bbbg/discord-client-id")) + + (set-backend! [:dir "/tmp/bbbg-secrets"]) + (secret "bbbg/discord-client-id") + + (set-backend! :pass) + (secret "bbbg/discord-client-id") + ) diff --git a/users/aspen/bbbg/src/bbbg/util/display.clj b/users/aspen/bbbg/src/bbbg/util/display.clj new file mode 100644 index 000000000000..40716632a3c9 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/util/display.clj @@ -0,0 +1,23 @@ +(ns bbbg.util.display + (:require + [bbbg.util.time :as t]) + (:import + [java.time.format DateTimeFormatter FormatStyle])) + +(defn format-date + ([d] (format-date d FormatStyle/MEDIUM)) + ([d ^FormatStyle format-style] + (let [formatter (DateTimeFormatter/ofLocalizedDate format-style)] + (.format (t/->LocalDate d) formatter)))) + +(defn pluralize + ([n sing plur] + (str (or n 0) " " (if (= 1 n) sing plur))) + ([n sing] + (pluralize n sing (str sing "s")))) + +(comment + (format-date #inst "2021-12-19T05:00:00.000-00:00") + (format-date #inst "2021-12-19T05:00:00.000-00:00" + FormatStyle/FULL) + ) diff --git a/users/aspen/bbbg/src/bbbg/util/spec.clj b/users/aspen/bbbg/src/bbbg/util/spec.clj new file mode 100644 index 000000000000..89ac92669914 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/util/spec.clj @@ -0,0 +1,16 @@ +(ns bbbg.util.spec + (:require [expound.alpha :as exp] + [clojure.spec.alpha :as s])) + +(defn assert! + ([spec s] (assert! "Spec assertion failed" spec s)) + ([message spec x] + (if (s/valid? spec x) + x + (throw (ex-info + (str message + "\n" + (exp/expound-str spec x)) + (assoc (s/explain-data spec x) + ::s/failure + ::s/assertion-failed)))))) diff --git a/users/aspen/bbbg/src/bbbg/util/sql.clj b/users/aspen/bbbg/src/bbbg/util/sql.clj new file mode 100644 index 000000000000..988959fd0603 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/util/sql.clj @@ -0,0 +1,5 @@ +(ns bbbg.util.sql + (:require [honeysql.core :as hsql])) + +(defn count-where [cond] + (hsql/call :count (hsql/call :case cond #sql/raw "1" :else nil))) diff --git a/users/aspen/bbbg/src/bbbg/util/time.clj b/users/aspen/bbbg/src/bbbg/util/time.clj new file mode 100644 index 000000000000..0278f89f5edd --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/util/time.clj @@ -0,0 +1,152 @@ +(ns bbbg.util.time + "Utilities for dealing with date/time" + (:require [clojure.spec.alpha :as s] + [clojure.test.check.generators :as gen] + [java-time :as jt]) + (:import [java.time + LocalDateTime LocalTime OffsetDateTime ZoneId ZoneOffset + LocalDate Year] + [java.time.format DateTimeFormatter DateTimeParseException] + java.util.Calendar + org.apache.commons.lang3.time.DurationFormatUtils)) + +(set! *warn-on-reflection* true) + +(defprotocol ToOffsetDateTime + (->OffsetDateTime [this] + "Coerces its argument to a `java.time.OffsetDateTime`")) + +(extend-protocol ToOffsetDateTime + OffsetDateTime + (->OffsetDateTime [odt] odt) + + java.util.Date + (->OffsetDateTime [d] + (-> d + .toInstant + (OffsetDateTime/ofInstant (ZoneId/of "UTC"))))) + +(defprotocol ToLocalTime (->LocalTime [this])) +(extend-protocol ToLocalTime + LocalTime + (->LocalTime [lt] lt) + + java.sql.Time + (->LocalTime [t] + (let [^Calendar cal (doto (Calendar/getInstance) + (.setTime t))] + (LocalTime/of + (.get cal Calendar/HOUR_OF_DAY) + (.get cal Calendar/MINUTE) + (.get cal Calendar/SECOND)))) + + java.util.Date + (->LocalTime [d] + (-> d .toInstant (LocalTime/ofInstant (ZoneId/of "UTC"))))) + +(defn local-time? [x] (satisfies? ToLocalTime x)) +(s/def ::local-time + (s/with-gen local-time? + #(gen/let [hour (gen/choose 0 23) + minute (gen/choose 0 59) + second (gen/choose 0 59) + nanos gen/nat] + (LocalTime/of hour minute second nanos)))) + +(defprotocol ToLocalDate (->LocalDate [this])) +(extend-protocol ToLocalDate + LocalDate + (->LocalDate [ld] ld) + + java.sql.Date + (->LocalDate [sd] (.toLocalDate sd)) + + java.util.Date + (->LocalDate [d] + (-> d .toInstant (LocalDate/ofInstant (ZoneId/of "UTC"))))) + +(defn local-date? [x] (satisfies? ToLocalDate x)) +(s/def ::local-date + (s/with-gen local-date? + #(gen/let [year (gen/choose Year/MIN_VALUE Year/MAX_VALUE) + day (gen/choose 1 (if (.isLeap (Year/of year)) + 366 + 365))] + (LocalDate/ofYearDay year day)))) + +(extend-protocol Inst + OffsetDateTime + (inst-ms* [zdt] + (inst-ms* (.toInstant zdt))) + + LocalDateTime + (inst-ms* [^LocalDateTime ldt] + (inst-ms* (.toInstant ldt ZoneOffset/UTC)))) + +(let [formatter DateTimeFormatter/ISO_OFFSET_DATE_TIME] + (defn ^OffsetDateTime parse-iso-8601 + "Parse s as an iso-8601 datetime, returning nil if invalid" + [^String s] + (try + (OffsetDateTime/parse s formatter) + (catch DateTimeParseException _ nil))) + + (defn format-iso-8601 + "Format dt, which can be an OffsetDateTime or java.util.Date, as iso-8601" + [dt] + (some->> dt ->OffsetDateTime (.format formatter)))) + +(let [formatter DateTimeFormatter/ISO_TIME] + (defn parse-iso-8601-time + "Parse s as an iso-8601 timestamp, returning nil if invalid" + [^String s] + (try + (LocalTime/parse s formatter) + (catch DateTimeParseException _ nil))) + + (defn format-iso-8601-time + "Format lt, which can be a LocalTime or java.sql.Time, as an iso-8601 + formatted timestamp without a date." + [lt] + (some->> lt ->LocalTime (.format formatter)))) + +(defmethod print-dup LocalTime [t w] + (binding [*out* w] + (print "#local-time ") + (print (str "\"" (format-iso-8601-time t) "\"")))) + +(defmethod print-method LocalTime [t w] + (print-dup t w)) + +(let [formatter DateTimeFormatter/ISO_LOCAL_DATE] + (defn parse-iso-8601-date + "Parse s as an iso-8601 date, returning nil if invalid" + [^String s] + (try + (LocalDate/parse s formatter) + (catch DateTimeParseException _ nil))) + + (defn format-iso-8601-date + "Format lt, which can be a LocalDate, as an iso-8601 formatted date without + a timestamp." + [lt] + (some->> lt ->LocalDate (.format formatter)))) + +(defmethod print-dup LocalDate [t w] + (binding [*out* w] + (print "#local-date ") + (print (str "\"" (format-iso-8601-date t) "\"")))) + +(defmethod print-method LocalDate [t w] + (print-dup t w)) + + +(defn ^String human-format-duration + "Human-format the given duration" + [^java.time.Duration dur] + (DurationFormatUtils/formatDurationWords (Math/abs (.toMillis dur)) true true)) + +(comment + (human-format-duration (jt/hours 5)) + (human-format-duration (jt/plus (jt/hours 5) (jt/minutes 7))) + ) diff --git a/users/aspen/bbbg/src/bbbg/views/flash.clj b/users/aspen/bbbg/src/bbbg/views/flash.clj new file mode 100644 index 000000000000..a44b21d4cb24 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/views/flash.clj @@ -0,0 +1,39 @@ +(ns bbbg.views.flash + (:require [clojure.spec.alpha :as s])) + +(s/def :flash/type #{:success :error :warning :info}) +(s/def :flash/message string?) +(s/def ::flash (s/keys :req [:flash/type :flash/message])) +(s/fdef add-flash :args (s/cat :resp map? :flash ::flash) :ret map?) + +;;; + +(def ^:dynamic *flash* nil) + +(defn wrap-page-flash [handler] + (fn + ([request] + (binding [*flash* (:flash request)] + (handler request))) + ([request respond raise] + (binding [*flash* (:flash request)] + (handler request respond raise))))) + +(defn add-flash [resp flash] + (update-in resp [:flash :flash/messages] conj flash)) + +(defn render-flash + ([] (render-flash *flash*)) + ([flash] + (when-some [messages (not-empty (:flash/messages flash))] + [:ul.flash-messages + (for [message messages] + [:li.flash-message + {:class (str "flash-" (-> message :flash/type name))} + (:flash/message message)])]))) + +(def test-flash + {:flash/messages + (for [type [:success :error :warning :info]] + {:flash/type type + :flash/message (str "Sample " type " message")})}) diff --git a/users/aspen/bbbg/src/bbbg/web.clj b/users/aspen/bbbg/src/bbbg/web.clj new file mode 100644 index 000000000000..f9755577a570 --- /dev/null +++ b/users/aspen/bbbg/src/bbbg/web.clj @@ -0,0 +1,140 @@ +(ns bbbg.web + (:require + [bbbg.discord.auth :as discord.auth :refer [wrap-discord-auth]] + [bbbg.handlers.attendee-checks :as attendee-checks] + [bbbg.handlers.attendees :as attendees] + [bbbg.handlers.core :refer [wrap-current-uri wrap-dynamic-auth]] + [bbbg.handlers.events :as events] + [bbbg.handlers.home :as home] + [bbbg.handlers.signup-form :as signup-form] + [bbbg.styles :refer [stylesheet]] + [bbbg.util.core :as u] + [bbbg.views.flash :refer [wrap-page-flash]] + [cambium.core :as log] + clj-time.coerce + [clojure.java.io :as io] + [clojure.spec.alpha :as s] + [com.stuartsierra.component :as component] + [compojure.core :refer [GET routes]] + [config.core :refer [env]] + [org.httpkit.server :as http-kit] + [ring.logger :refer [wrap-with-logger]] + [ring.middleware.flash :refer [wrap-flash]] + [ring.middleware.keyword-params :refer [wrap-keyword-params]] + [ring.middleware.multipart-params :refer [wrap-multipart-params]] + [ring.middleware.params :refer [wrap-params]] + [ring.middleware.resource :refer [wrap-resource]] + [ring.middleware.session :refer [wrap-session]] + [ring.middleware.session.cookie :refer [cookie-store]] + [ring.util.response :refer [content-type response]]) + (:import + java.util.Base64)) + +(s/def ::port pos-int?) + +(s/def ::cookie-secret + (s/and bytes? #(= 16 (count %)))) + +(s/def ::config + (s/merge + (s/keys :req [::port] + :opt [::cookie-secret + ::base-url]) + ::discord.auth/config)) + +(s/fdef make-server + :args (s/cat :config ::config)) + + +(defn- string->cookie-secret [raw] + (s/assert + ::cookie-secret + (when raw + (.decode (Base64/getDecoder) + (.getBytes raw "UTF-8"))))) + +(defn env->config [] + (s/assert + ::config + (u/remove-nils + (merge + {::port (:port env 8888) + ::cookie-secret (some-> env :cookie-secret string->cookie-secret) + ::base-url (:base-url env)} + (discord.auth/env->config))))) + +(defn dev-config [] + (s/assert + ::config + (merge + {::port 8888 + ::cookie-secret (into-array Byte/TYPE (repeat 16 0))} + (discord.auth/dev-config)))) + +;;; + +(defn app-routes [env] + (routes + (GET "/main.css" [] + (-> (response + (str + "\n/* begin base.css */\n" + (slurp (io/resource "base.css")) + "\n/* end base.css */\n" + stylesheet)) + (content-type "text/css"))) + + (attendees/attendees-routes env) + (attendee-checks/attendee-checks-routes env) + (signup-form/signup-form-routes env) + (events/events-routes env) + (home/home-routes env))) + +(defn middleware [app env] + (-> app + (wrap-resource "public") + (wrap-with-logger + {:log-fn + (fn [{:keys [level throwable message]}] + (log/log level {} throwable message))}) + wrap-current-uri + wrap-dynamic-auth + (wrap-discord-auth env) + wrap-keyword-params + wrap-multipart-params + wrap-params + wrap-page-flash + wrap-flash + (wrap-session {:store (cookie-store + {:key (:cookie-secret env) + :readers {'clj-time/date-time + clj-time.coerce/from-string}}) + :cookie-attrs {:same-site :lax}}))) + +(defn handler [env] + (-> (app-routes env) + (middleware env))) + +(defrecord WebServer [port cookie-secret db] + component/Lifecycle + (start [this] + (assoc this + ::shutdown-fn + (http-kit/run-server + (fn [r] ((handler this) r)) + {:port port}))) + (stop [this] + (if-let [shutdown-fn (::shutdown-fn this)] + (do (shutdown-fn :timeout 100) + (dissoc this ::shutdown-fn)) + this))) + +(defn make-server [{::keys [port cookie-secret] + :as env}] + (component/using + (map->WebServer + (merge + {:port port + :cookie-secret cookie-secret} + env)) + [:db])) diff --git a/users/aspen/bbbg/test/bbbg/meetup/import_test.clj b/users/aspen/bbbg/test/bbbg/meetup/import_test.clj new file mode 100644 index 000000000000..d7d698a58c8d --- /dev/null +++ b/users/aspen/bbbg/test/bbbg/meetup/import_test.clj @@ -0,0 +1,7 @@ +(ns bbbg.meetup.import-test + (:require [bbbg.meetup.import :as sut] + [clojure.test :refer :all])) + +(deftest test-row-user-id->user-id + (is (= "246364067" (sut/row-user-id->user-id "user 246364067"))) + (is (= "246364067" (sut/row-user-id->user-id "246364067")))) diff --git a/users/aspen/bbbg/tf.nix b/users/aspen/bbbg/tf.nix new file mode 100644 index 000000000000..e6ea69dfd01e --- /dev/null +++ b/users/aspen/bbbg/tf.nix @@ -0,0 +1,96 @@ +{ depot, ... }: + +let + inherit (depot.users.aspen) + terraform + ; + +in +terraform.workspace "bbbg" +{ + plugins = (p: with p; [ + aws + cloudflare + ]); +} +{ + machine = terraform.nixosMachine { + name = "bbbg"; + instanceType = "t3a.small"; + rootVolumeSizeGb = 250; + extraIngressPorts = [ 80 443 ]; + configuration = { pkgs, lib, config, depot, ... }: { + imports = [ + ./module.nix + "${depot.third_party.agenix.src}/modules/age.nix" + ]; + + services.openssh.enable = true; + + services.nginx = { + enable = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedProxySettings = true; + }; + + networking.firewall.enable = false; + + programs.zsh.enable = true; + + users.users.grfn = { + isNormalUser = true; + initialPassword = "password"; + extraGroups = [ + "wheel" + "networkmanager" + "audio" + "docker" + ]; + shell = pkgs.zsh; + openssh.authorizedKeys.keys = [ + depot.users.aspen.keys.main + ]; + }; + + security.sudo.extraRules = [{ + groups = [ "wheel" ]; + commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }]; + }]; + + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + + age.secrets = { + bbbg.file = + depot.users.aspen.secrets."bbbg.age"; + }; + + services.bbbg.enable = true; + services.bbbg.database.enable = true; + services.bbbg.proxy.enable = true; + services.bbbg.domain = "bbbg.gws.fyi"; + + security.acme.defaults.email = "root@gws.fyi"; + security.acme.acceptTerms = true; + }; + }; + + dns = { + data.cloudflare_zone.gws-fyi = { + name = "gws.fyi"; + }; + + resource.cloudflare_record.bbbg = { + zone_id = "\${data.cloudflare_zone.gws-fyi.id}"; + name = "bbbg"; + type = "A"; + value = "\${aws_instance.bbbg_machine.public_ip}"; + proxied = false; + }; + }; +} |