The code and data behind xeiaso.net
0
fork

Configure Feed

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

Rewrite site backend in Rust (#178)

* add shell.nix changes for Rust #176

* set up base crate layout

* add first set of dependencies

* start adding basic app modules

* start html templates

* serve index page

* add contact and feeds pages

* add resume rendering support

* resume cleanups

* get signalboost page working

* rewrite config to be in dhall

* more work

* basic generic post loading

* more tests

* initial blog index support

* fix routing?

* render blogposts

* X-Clacks-Overhead

* split blog handlers into blog.rs

* gallery index

* gallery posts

* fix hashtags

* remove instantpage (it messes up the metrics)

* talk support + prometheus

* Create rust.yml

* Update rust.yml

* Update codeql-analysis.yml

* add jsonfeed library

* jsonfeed support

* rss/atom

* go mod tidy

* atom: add posted date

* rss: add publishing date

* nix: build rust program

* rip out go code

* rip out go templates

* prepare for serving in docker

* create kubernetes deployment

* create automagic deployment

* build docker images on non-master

* more fixes

* fix timestamps

* fix RSS/Atom/JSONFeed validation errors

* add go vanity import redirecting

* templates/header: remove this

* atom feed: fixes

* fix?

* fix??

* fix rust tests

* Update rust.yml

* automatically show snow during the winter

* fix dates

* show commit link in footer

* sitemap support

* fix compiler warning

* start basic patreon client

* integrate kankyo

* fix patreon client

* add patrons page

* remove this

* handle patron errors better

* fix build

* clean up deploy

* sort envvars for deploy

* remove deps.nix

* shell.nix: remove go

* update README

* fix envvars for tests

* nice

* blog: add rewrite in rust post

* blog/site-update: more words

authored by

Christine Dodrill and committed by
GitHub
385d25c9 449e9342

+6557 -3499
-1
.gitattributes
··· 1 - nix/deps.nix linguist-vendored 2 1 nix/sources.nix linguist-vendored
-39
.github/workflows/codeql-analysis.yml
··· 1 - name: "Code scanning - action" 2 - 3 - on: 4 - push: 5 - pull_request: 6 - schedule: 7 - - cron: '0 18 * * 6' 8 - 9 - jobs: 10 - CodeQL-Build: 11 - runs-on: ubuntu-latest 12 - 13 - steps: 14 - - name: Checkout repository 15 - uses: actions/checkout@v2 16 - with: 17 - # We must fetch at least the immediate parents so that if this is 18 - # a pull request then we can checkout the head. 19 - fetch-depth: 2 20 - 21 - # If this run was triggered by a pull request event, then checkout 22 - # the head of the pull request instead of the merge commit. 23 - - run: git checkout HEAD^2 24 - if: ${{ github.event_name == 'pull_request' }} 25 - 26 - # Initializes the CodeQL tools for scanning. 27 - - name: Initialize CodeQL 28 - uses: github/codeql-action/init@v1 29 - # Override language selection by uncommenting this and choosing your languages 30 - with: 31 - languages: go 32 - 33 - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 34 - # If this step fails, then you should remove it and run the build manually (see below) 35 - - name: Autobuild 36 - uses: github/codeql-action/autobuild@v1 37 - 38 - - name: Perform CodeQL Analysis 39 - uses: github/codeql-action/analyze@v1
-21
.github/workflows/go.yml
··· 1 - name: Go 2 - on: 3 - - push 4 - - pull_request 5 - jobs: 6 - build: 7 - name: Build 8 - runs-on: ubuntu-latest 9 - steps: 10 - - name: Set up Go 1.14 11 - uses: actions/setup-go@v1 12 - with: 13 - go-version: 1.14 14 - id: go 15 - - name: Check out code into the Go module directory 16 - uses: actions/checkout@v1 17 - - name: Test 18 - run: go test -v ./... 19 - env: 20 - GO111MODULE: on 21 - GOPROXY: https://cache.greedo.xeserv.us
-80
.github/workflows/kubernetes-cd.yml
··· 1 - name: "CI/CD" 2 - on: 3 - push: 4 - branches: 5 - - master 6 - jobs: 7 - deploy: 8 - runs-on: ubuntu-latest 9 - steps: 10 - - uses: actions/checkout@v1 11 - - name: Build container image 12 - run: | 13 - docker build -t xena/christinewebsite:$(echo $GITHUB_SHA | head -c7) . 14 - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin 15 - docker push xena/christinewebsite 16 - env: 17 - DOCKER_USERNAME: "xena" 18 - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 19 - - name: Download secrets/Install/Configure/Use Dyson 20 - run: | 21 - mkdir ~/.ssh 22 - echo $FILE_DATA | base64 -d > ~/.ssh/id_rsa 23 - md5sum ~/.ssh/id_rsa 24 - chmod 600 ~/.ssh/id_rsa 25 - git clone git@ssh.tulpa.dev:cadey/within-terraform-secret 26 - curl https://xena.greedo.xeserv.us/files/dyson-linux-amd64-0.1.0.tgz | tar xz 27 - cp ./dyson-linux-amd64-0.1.1/dyson . 28 - rm -rf dyson-linux-amd64-0.1.1 29 - mkdir -p ~/.config/dyson 30 - 31 - echo '[DigitalOcean] 32 - Token = "" 33 - 34 - [Cloudflare] 35 - Email = "" 36 - Token = "" 37 - 38 - [Secrets] 39 - GitCheckout = "./within-terraform-secret"' > ~/.config/dyson/dyson.ini 40 - 41 - ./dyson manifest \ 42 - --name=christinewebsite \ 43 - --domain=christine.website \ 44 - --dockerImage=xena/christinewebsite:$(echo $GITHUB_SHA | head -c7) \ 45 - --containerPort=5000 \ 46 - --replicas=2 \ 47 - --useProdLE=true > $GITHUB_WORKSPACE/deploy.yml 48 - env: 49 - FILE_DATA: ${{ secrets.SSH_PRIVATE_KEY }} 50 - GIT_SSH_COMMAND: "ssh -i ~/.ssh/id_rsa -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" 51 - - name: Save DigitalOcean kubeconfig 52 - uses: digitalocean/action-doctl@master 53 - env: 54 - DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} 55 - with: 56 - args: kubernetes cluster kubeconfig show kubermemes > $GITHUB_WORKSPACE/.kubeconfig 57 - - name: Deploy to DigitalOcean Kubernetes 58 - uses: docker://lachlanevenson/k8s-kubectl 59 - with: 60 - args: --kubeconfig=/github/workspace/.kubeconfig apply -n apps -f /github/workspace/deploy.yml 61 - - name: Verify deployment 62 - uses: docker://lachlanevenson/k8s-kubectl 63 - with: 64 - args: --kubeconfig=/github/workspace/.kubeconfig rollout status -n apps deployment/christinewebsite 65 - - name: Ping Google 66 - uses: docker://lachlanevenson/k8s-kubectl 67 - with: 68 - args: --kubeconfig=/github/workspace/.kubeconfig apply -f /github/workspace/k8s/job.yml 69 - - name: Sleep 70 - run: | 71 - sleep 5 72 - - name: Don't Ping Google 73 - uses: docker://lachlanevenson/k8s-kubectl 74 - with: 75 - args: --kubeconfig=/github/workspace/.kubeconfig delete -f /github/workspace/k8s/job.yml 76 - - name: POSSE 77 - env: 78 - MI_TOKEN: ${{ secrets.MI_TOKEN }} 79 - run: | 80 - curl -H "Authorization: $MI_TOKEN" --data "https://christine.website/blog.json" https://mi.within.website/blog/refresh
+36 -10
.github/workflows/nix.yml
··· 1 1 name: "Nix" 2 2 on: 3 3 push: 4 + branches: 5 + - master 6 + pull_request: 7 + branches: 8 + - master 4 9 jobs: 5 - tests: 10 + docker-build: 6 11 runs-on: ubuntu-latest 7 12 steps: 8 - - uses: actions/checkout@v1 9 - - uses: cachix/install-nix-action@v6 10 - - uses: cachix/cachix-action@v3 11 - with: 12 - name: xe 13 - - run: | 14 - nix-build docker.nix 15 - docker load -i result 16 - docker tag xena/christinewebsite:latest xena/christinewebsite:$(echo $GITHUB_SHA | head -c7) 13 + - uses: actions/checkout@v1 14 + - uses: cachix/install-nix-action@v6 15 + - uses: cachix/cachix-action@v3 16 + with: 17 + name: xe 18 + signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' 19 + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 20 + - run: | 21 + docker load -i result 22 + docker tag xena/christinewebsite:latest xena/christinewebsite:$GITHUB_SHA 23 + echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin 24 + docker push xena/christinewebsite 25 + env: 26 + DOCKER_USERNAME: "xena" 27 + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 28 + release: 29 + runs-on: ubuntu-latest 30 + needs: docker-build 31 + if: github.ref == 'refs/heads/master' 32 + steps: 33 + - uses: cachix/install-nix-action@v6 34 + - name: deploy 35 + run: ./scripts/release.sh 36 + env: 37 + DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} 38 + MI_TOKEN: ${{ secrets.MI_TOKEN }} 39 + PATREON_ACCESS_TOKEN: ${{ secrets.PATREON_ACCESS_TOKEN }} 40 + PATREON_CLIENT_ID: ${{ secrets.PATREON_CLIENT_ID }} 41 + PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }} 42 + PATREON_REFRESH_TOKEN: ${{ secrets.PATREON_REFRESH_TOKEN }}
+25
.github/workflows/rust.yml
··· 1 + name: Rust 2 + on: 3 + push: 4 + branches: [ master ] 5 + pull_request: 6 + branches: [ master ] 7 + env: 8 + CARGO_TERM_COLOR: always 9 + jobs: 10 + build: 11 + runs-on: ubuntu-latest 12 + steps: 13 + - uses: actions/checkout@v2 14 + - name: Build 15 + run: cargo build --all 16 + - name: Run tests 17 + run: | 18 + cargo test 19 + (cd lib/jsonfeed && cargo test) 20 + (cd lib/patreon && cargo test) 21 + env: 22 + PATREON_ACCESS_TOKEN: ${{ secrets.PATREON_ACCESS_TOKEN }} 23 + PATREON_CLIENT_ID: ${{ secrets.PATREON_CLIENT_ID }} 24 + PATREON_CLIENT_SECRET: ${{ secrets.PATREON_CLIENT_SECRET }} 25 + PATREON_REFRESH_TOKEN: ${{ secrets.PATREON_REFRESH_TOKEN }}
+1 -1
.gitignore
··· 5 5 /result-* 6 6 /result 7 7 .#* 8 - 8 + /target
+2592
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + [[package]] 4 + name = "abnf" 5 + version = "0.6.1" 6 + source = "registry+https://github.com/rust-lang/crates.io-index" 7 + checksum = "47feb9fbcef700639ef28e04ca2a87eab8161a01a075ee227b15c90143805462" 8 + dependencies = [ 9 + "nom", 10 + ] 11 + 12 + [[package]] 13 + name = "abnf_to_pest" 14 + version = "0.5.0" 15 + source = "registry+https://github.com/rust-lang/crates.io-index" 16 + checksum = "372baaa5d3a422d8816b513bcdb2c120078c8614f7ecbcc3baf34a1634bbbe2e" 17 + dependencies = [ 18 + "abnf", 19 + "indexmap", 20 + "itertools", 21 + "pretty", 22 + ] 23 + 24 + [[package]] 25 + name = "addr2line" 26 + version = "0.13.0" 27 + source = "registry+https://github.com/rust-lang/crates.io-index" 28 + checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" 29 + dependencies = [ 30 + "gimli", 31 + ] 32 + 33 + [[package]] 34 + name = "adler" 35 + version = "0.2.3" 36 + source = "registry+https://github.com/rust-lang/crates.io-index" 37 + checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" 38 + 39 + [[package]] 40 + name = "adler32" 41 + version = "1.1.0" 42 + source = "registry+https://github.com/rust-lang/crates.io-index" 43 + checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" 44 + 45 + [[package]] 46 + name = "aho-corasick" 47 + version = "0.7.13" 48 + source = "registry+https://github.com/rust-lang/crates.io-index" 49 + checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 50 + dependencies = [ 51 + "memchr", 52 + ] 53 + 54 + [[package]] 55 + name = "annotate-snippets" 56 + version = "0.7.0" 57 + source = "registry+https://github.com/rust-lang/crates.io-index" 58 + checksum = "aba2d96b8c8b5e656ad7ffb0d09f57772f10a1db74c8d23fca0ec695b38a4047" 59 + 60 + [[package]] 61 + name = "ansi_term" 62 + version = "0.11.0" 63 + source = "registry+https://github.com/rust-lang/crates.io-index" 64 + checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 65 + dependencies = [ 66 + "winapi 0.3.9", 67 + ] 68 + 69 + [[package]] 70 + name = "anyhow" 71 + version = "1.0.31" 72 + source = "registry+https://github.com/rust-lang/crates.io-index" 73 + checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" 74 + 75 + [[package]] 76 + name = "arrayvec" 77 + version = "0.5.1" 78 + source = "registry+https://github.com/rust-lang/crates.io-index" 79 + checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" 80 + 81 + [[package]] 82 + name = "atom_syndication" 83 + version = "0.9.0" 84 + source = "registry+https://github.com/rust-lang/crates.io-index" 85 + checksum = "7d0b2fa7aedc48c4fbe1d38b25c1462a6e7b962397f27a3a8d7cbb1c08f0008b" 86 + dependencies = [ 87 + "chrono", 88 + "derive_builder", 89 + "diligent-date-parser", 90 + "quick-xml 0.18.1", 91 + "serde", 92 + ] 93 + 94 + [[package]] 95 + name = "atty" 96 + version = "0.2.14" 97 + source = "registry+https://github.com/rust-lang/crates.io-index" 98 + checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 99 + dependencies = [ 100 + "hermit-abi", 101 + "libc", 102 + "winapi 0.3.9", 103 + ] 104 + 105 + [[package]] 106 + name = "autocfg" 107 + version = "0.1.7" 108 + source = "registry+https://github.com/rust-lang/crates.io-index" 109 + checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" 110 + 111 + [[package]] 112 + name = "autocfg" 113 + version = "1.0.0" 114 + source = "registry+https://github.com/rust-lang/crates.io-index" 115 + checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 116 + 117 + [[package]] 118 + name = "backtrace" 119 + version = "0.3.50" 120 + source = "registry+https://github.com/rust-lang/crates.io-index" 121 + checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293" 122 + dependencies = [ 123 + "addr2line", 124 + "cfg-if", 125 + "libc", 126 + "miniz_oxide", 127 + "object", 128 + "rustc-demangle", 129 + ] 130 + 131 + [[package]] 132 + name = "base64" 133 + version = "0.11.0" 134 + source = "registry+https://github.com/rust-lang/crates.io-index" 135 + checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 136 + 137 + [[package]] 138 + name = "base64" 139 + version = "0.12.3" 140 + source = "registry+https://github.com/rust-lang/crates.io-index" 141 + checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" 142 + 143 + [[package]] 144 + name = "bitflags" 145 + version = "1.2.1" 146 + source = "registry+https://github.com/rust-lang/crates.io-index" 147 + checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 148 + 149 + [[package]] 150 + name = "block-buffer" 151 + version = "0.7.3" 152 + source = "registry+https://github.com/rust-lang/crates.io-index" 153 + checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" 154 + dependencies = [ 155 + "block-padding", 156 + "byte-tools", 157 + "byteorder", 158 + "generic-array", 159 + ] 160 + 161 + [[package]] 162 + name = "block-padding" 163 + version = "0.1.5" 164 + source = "registry+https://github.com/rust-lang/crates.io-index" 165 + checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" 166 + dependencies = [ 167 + "byte-tools", 168 + ] 169 + 170 + [[package]] 171 + name = "blocking" 172 + version = "0.4.6" 173 + source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "9d17efb70ce4421e351d61aafd90c16a20fb5bfe339fcdc32a86816280e62ce0" 175 + dependencies = [ 176 + "futures-channel", 177 + "futures-util", 178 + "once_cell", 179 + "parking", 180 + "waker-fn", 181 + ] 182 + 183 + [[package]] 184 + name = "buf_redux" 185 + version = "0.8.4" 186 + source = "registry+https://github.com/rust-lang/crates.io-index" 187 + checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" 188 + dependencies = [ 189 + "memchr", 190 + "safemem", 191 + ] 192 + 193 + [[package]] 194 + name = "bumpalo" 195 + version = "3.4.0" 196 + source = "registry+https://github.com/rust-lang/crates.io-index" 197 + checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" 198 + 199 + [[package]] 200 + name = "byte-tools" 201 + version = "0.3.1" 202 + source = "registry+https://github.com/rust-lang/crates.io-index" 203 + checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" 204 + 205 + [[package]] 206 + name = "bytecount" 207 + version = "0.6.0" 208 + source = "registry+https://github.com/rust-lang/crates.io-index" 209 + checksum = "b0017894339f586ccb943b01b9555de56770c11cda818e7e3d8bd93f4ed7f46e" 210 + 211 + [[package]] 212 + name = "byteorder" 213 + version = "1.3.4" 214 + source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 216 + 217 + [[package]] 218 + name = "bytes" 219 + version = "0.5.6" 220 + source = "registry+https://github.com/rust-lang/crates.io-index" 221 + checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" 222 + 223 + [[package]] 224 + name = "cc" 225 + version = "1.0.58" 226 + source = "registry+https://github.com/rust-lang/crates.io-index" 227 + checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" 228 + 229 + [[package]] 230 + name = "cfg-if" 231 + version = "0.1.10" 232 + source = "registry+https://github.com/rust-lang/crates.io-index" 233 + checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 234 + 235 + [[package]] 236 + name = "chrono" 237 + version = "0.4.13" 238 + source = "registry+https://github.com/rust-lang/crates.io-index" 239 + checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" 240 + dependencies = [ 241 + "num-integer", 242 + "num-traits", 243 + "serde", 244 + "time", 245 + ] 246 + 247 + [[package]] 248 + name = "chrono_utils" 249 + version = "0.1.3" 250 + source = "registry+https://github.com/rust-lang/crates.io-index" 251 + checksum = "7f69ed74e2117892a1a4e05d31f6612178e8e827bfbd83bbf8ca8c1bcfbda710" 252 + dependencies = [ 253 + "chrono", 254 + ] 255 + 256 + [[package]] 257 + name = "clap" 258 + version = "2.33.1" 259 + source = "registry+https://github.com/rust-lang/crates.io-index" 260 + checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" 261 + dependencies = [ 262 + "ansi_term", 263 + "atty", 264 + "bitflags", 265 + "strsim 0.8.0", 266 + "textwrap", 267 + "unicode-width", 268 + "vec_map", 269 + ] 270 + 271 + [[package]] 272 + name = "cloudabi" 273 + version = "0.0.3" 274 + source = "registry+https://github.com/rust-lang/crates.io-index" 275 + checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 276 + dependencies = [ 277 + "bitflags", 278 + ] 279 + 280 + [[package]] 281 + name = "comrak" 282 + version = "0.8.0" 283 + source = "registry+https://github.com/rust-lang/crates.io-index" 284 + checksum = "b818732a109eeabbe99fee28030ff8ecbd606889fcd25509ed933e6c69b7aa69" 285 + dependencies = [ 286 + "clap", 287 + "entities", 288 + "lazy_static", 289 + "pest", 290 + "pest_derive", 291 + "regex", 292 + "twoway 0.2.1", 293 + "typed-arena", 294 + "unicode_categories", 295 + ] 296 + 297 + [[package]] 298 + name = "core-foundation" 299 + version = "0.7.0" 300 + source = "registry+https://github.com/rust-lang/crates.io-index" 301 + checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" 302 + dependencies = [ 303 + "core-foundation-sys", 304 + "libc", 305 + ] 306 + 307 + [[package]] 308 + name = "core-foundation-sys" 309 + version = "0.7.0" 310 + source = "registry+https://github.com/rust-lang/crates.io-index" 311 + checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" 312 + 313 + [[package]] 314 + name = "crc32fast" 315 + version = "1.2.0" 316 + source = "registry+https://github.com/rust-lang/crates.io-index" 317 + checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" 318 + dependencies = [ 319 + "cfg-if", 320 + ] 321 + 322 + [[package]] 323 + name = "darling" 324 + version = "0.10.2" 325 + source = "registry+https://github.com/rust-lang/crates.io-index" 326 + checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" 327 + dependencies = [ 328 + "darling_core", 329 + "darling_macro", 330 + ] 331 + 332 + [[package]] 333 + name = "darling_core" 334 + version = "0.10.2" 335 + source = "registry+https://github.com/rust-lang/crates.io-index" 336 + checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" 337 + dependencies = [ 338 + "fnv", 339 + "ident_case", 340 + "proc-macro2", 341 + "quote", 342 + "strsim 0.9.3", 343 + "syn", 344 + ] 345 + 346 + [[package]] 347 + name = "darling_macro" 348 + version = "0.10.2" 349 + source = "registry+https://github.com/rust-lang/crates.io-index" 350 + checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" 351 + dependencies = [ 352 + "darling_core", 353 + "quote", 354 + "syn", 355 + ] 356 + 357 + [[package]] 358 + name = "derive_builder" 359 + version = "0.9.0" 360 + source = "registry+https://github.com/rust-lang/crates.io-index" 361 + checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" 362 + dependencies = [ 363 + "darling", 364 + "derive_builder_core", 365 + "proc-macro2", 366 + "quote", 367 + "syn", 368 + ] 369 + 370 + [[package]] 371 + name = "derive_builder_core" 372 + version = "0.9.0" 373 + source = "registry+https://github.com/rust-lang/crates.io-index" 374 + checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" 375 + dependencies = [ 376 + "darling", 377 + "proc-macro2", 378 + "quote", 379 + "syn", 380 + ] 381 + 382 + [[package]] 383 + name = "dhall" 384 + version = "0.5.3" 385 + source = "registry+https://github.com/rust-lang/crates.io-index" 386 + checksum = "c9a89818e4ddd4f9f466d2aadcdfae18d6cf27387dc044c345f011cd0d6e2f23" 387 + dependencies = [ 388 + "abnf_to_pest", 389 + "annotate-snippets", 390 + "blocking", 391 + "hex", 392 + "itertools", 393 + "lazy_static", 394 + "once_cell", 395 + "percent-encoding", 396 + "pest", 397 + "pest_consume", 398 + "pest_generator", 399 + "quote", 400 + "reqwest", 401 + "serde", 402 + "serde_cbor", 403 + "sha2", 404 + "smallvec", 405 + "url", 406 + "walkdir", 407 + ] 408 + 409 + [[package]] 410 + name = "dhall_proc_macros" 411 + version = "0.5.0" 412 + source = "registry+https://github.com/rust-lang/crates.io-index" 413 + checksum = "bf6cff1e2ddd03851652e0cde982b01dc877c9fc9da9ba25ad4241a151945f09" 414 + dependencies = [ 415 + "itertools", 416 + "proc-macro2", 417 + "quote", 418 + "syn", 419 + ] 420 + 421 + [[package]] 422 + name = "digest" 423 + version = "0.8.1" 424 + source = "registry+https://github.com/rust-lang/crates.io-index" 425 + checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" 426 + dependencies = [ 427 + "generic-array", 428 + ] 429 + 430 + [[package]] 431 + name = "diligent-date-parser" 432 + version = "0.1.1" 433 + source = "registry+https://github.com/rust-lang/crates.io-index" 434 + checksum = "28caca0eb64b9b22bdcab47424e0f7716af92d33ad035f765e5ec2b08cf14fcc" 435 + dependencies = [ 436 + "chrono", 437 + ] 438 + 439 + [[package]] 440 + name = "doc-comment" 441 + version = "0.3.3" 442 + source = "registry+https://github.com/rust-lang/crates.io-index" 443 + checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 444 + 445 + [[package]] 446 + name = "dtoa" 447 + version = "0.4.6" 448 + source = "registry+https://github.com/rust-lang/crates.io-index" 449 + checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" 450 + 451 + [[package]] 452 + name = "either" 453 + version = "1.5.3" 454 + source = "registry+https://github.com/rust-lang/crates.io-index" 455 + checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" 456 + 457 + [[package]] 458 + name = "encoding_rs" 459 + version = "0.8.23" 460 + source = "registry+https://github.com/rust-lang/crates.io-index" 461 + checksum = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" 462 + dependencies = [ 463 + "cfg-if", 464 + ] 465 + 466 + [[package]] 467 + name = "entities" 468 + version = "1.0.1" 469 + source = "registry+https://github.com/rust-lang/crates.io-index" 470 + checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" 471 + 472 + [[package]] 473 + name = "env_logger" 474 + version = "0.7.1" 475 + source = "registry+https://github.com/rust-lang/crates.io-index" 476 + checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 477 + dependencies = [ 478 + "atty", 479 + "humantime", 480 + "log 0.4.8", 481 + "regex", 482 + "termcolor", 483 + ] 484 + 485 + [[package]] 486 + name = "envy" 487 + version = "0.4.1" 488 + source = "registry+https://github.com/rust-lang/crates.io-index" 489 + checksum = "f938a4abd5b75fe3737902dbc2e79ca142cc1526827a9e40b829a086758531a9" 490 + dependencies = [ 491 + "serde", 492 + ] 493 + 494 + [[package]] 495 + name = "error-chain" 496 + version = "0.12.2" 497 + source = "registry+https://github.com/rust-lang/crates.io-index" 498 + checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" 499 + dependencies = [ 500 + "backtrace", 501 + "version_check 0.9.2", 502 + ] 503 + 504 + [[package]] 505 + name = "fake-simd" 506 + version = "0.1.2" 507 + source = "registry+https://github.com/rust-lang/crates.io-index" 508 + checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" 509 + 510 + [[package]] 511 + name = "fnv" 512 + version = "1.0.7" 513 + source = "registry+https://github.com/rust-lang/crates.io-index" 514 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 515 + 516 + [[package]] 517 + name = "foreign-types" 518 + version = "0.3.2" 519 + source = "registry+https://github.com/rust-lang/crates.io-index" 520 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 521 + dependencies = [ 522 + "foreign-types-shared", 523 + ] 524 + 525 + [[package]] 526 + name = "foreign-types-shared" 527 + version = "0.1.1" 528 + source = "registry+https://github.com/rust-lang/crates.io-index" 529 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 530 + 531 + [[package]] 532 + name = "fuchsia-cprng" 533 + version = "0.1.1" 534 + source = "registry+https://github.com/rust-lang/crates.io-index" 535 + checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 536 + 537 + [[package]] 538 + name = "fuchsia-zircon" 539 + version = "0.3.3" 540 + source = "registry+https://github.com/rust-lang/crates.io-index" 541 + checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 542 + dependencies = [ 543 + "bitflags", 544 + "fuchsia-zircon-sys", 545 + ] 546 + 547 + [[package]] 548 + name = "fuchsia-zircon-sys" 549 + version = "0.3.3" 550 + source = "registry+https://github.com/rust-lang/crates.io-index" 551 + checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 552 + 553 + [[package]] 554 + name = "futures" 555 + version = "0.3.5" 556 + source = "registry+https://github.com/rust-lang/crates.io-index" 557 + checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" 558 + dependencies = [ 559 + "futures-channel", 560 + "futures-core", 561 + "futures-executor", 562 + "futures-io", 563 + "futures-sink", 564 + "futures-task", 565 + "futures-util", 566 + ] 567 + 568 + [[package]] 569 + name = "futures-channel" 570 + version = "0.3.5" 571 + source = "registry+https://github.com/rust-lang/crates.io-index" 572 + checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" 573 + dependencies = [ 574 + "futures-core", 575 + "futures-sink", 576 + ] 577 + 578 + [[package]] 579 + name = "futures-core" 580 + version = "0.3.5" 581 + source = "registry+https://github.com/rust-lang/crates.io-index" 582 + checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" 583 + 584 + [[package]] 585 + name = "futures-executor" 586 + version = "0.3.5" 587 + source = "registry+https://github.com/rust-lang/crates.io-index" 588 + checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" 589 + dependencies = [ 590 + "futures-core", 591 + "futures-task", 592 + "futures-util", 593 + ] 594 + 595 + [[package]] 596 + name = "futures-io" 597 + version = "0.3.5" 598 + source = "registry+https://github.com/rust-lang/crates.io-index" 599 + checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" 600 + 601 + [[package]] 602 + name = "futures-macro" 603 + version = "0.3.5" 604 + source = "registry+https://github.com/rust-lang/crates.io-index" 605 + checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" 606 + dependencies = [ 607 + "proc-macro-hack", 608 + "proc-macro2", 609 + "quote", 610 + "syn", 611 + ] 612 + 613 + [[package]] 614 + name = "futures-sink" 615 + version = "0.3.5" 616 + source = "registry+https://github.com/rust-lang/crates.io-index" 617 + checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" 618 + 619 + [[package]] 620 + name = "futures-task" 621 + version = "0.3.5" 622 + source = "registry+https://github.com/rust-lang/crates.io-index" 623 + checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" 624 + dependencies = [ 625 + "once_cell", 626 + ] 627 + 628 + [[package]] 629 + name = "futures-util" 630 + version = "0.3.5" 631 + source = "registry+https://github.com/rust-lang/crates.io-index" 632 + checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" 633 + dependencies = [ 634 + "futures-channel", 635 + "futures-core", 636 + "futures-io", 637 + "futures-macro", 638 + "futures-sink", 639 + "futures-task", 640 + "memchr", 641 + "pin-project", 642 + "pin-utils", 643 + "proc-macro-hack", 644 + "proc-macro-nested", 645 + "slab", 646 + ] 647 + 648 + [[package]] 649 + name = "generic-array" 650 + version = "0.12.3" 651 + source = "registry+https://github.com/rust-lang/crates.io-index" 652 + checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" 653 + dependencies = [ 654 + "typenum", 655 + ] 656 + 657 + [[package]] 658 + name = "getrandom" 659 + version = "0.1.14" 660 + source = "registry+https://github.com/rust-lang/crates.io-index" 661 + checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 662 + dependencies = [ 663 + "cfg-if", 664 + "libc", 665 + "wasi", 666 + ] 667 + 668 + [[package]] 669 + name = "gimli" 670 + version = "0.22.0" 671 + source = "registry+https://github.com/rust-lang/crates.io-index" 672 + checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" 673 + 674 + [[package]] 675 + name = "glob" 676 + version = "0.3.0" 677 + source = "registry+https://github.com/rust-lang/crates.io-index" 678 + checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 679 + 680 + [[package]] 681 + name = "go_vanity" 682 + version = "0.1.0" 683 + dependencies = [ 684 + "mime 0.3.16", 685 + "ructe", 686 + "warp", 687 + ] 688 + 689 + [[package]] 690 + name = "h2" 691 + version = "0.2.6" 692 + source = "registry+https://github.com/rust-lang/crates.io-index" 693 + checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" 694 + dependencies = [ 695 + "bytes", 696 + "fnv", 697 + "futures-core", 698 + "futures-sink", 699 + "futures-util", 700 + "http", 701 + "indexmap", 702 + "slab", 703 + "tokio", 704 + "tokio-util", 705 + "tracing", 706 + ] 707 + 708 + [[package]] 709 + name = "half" 710 + version = "1.6.0" 711 + source = "registry+https://github.com/rust-lang/crates.io-index" 712 + checksum = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177" 713 + 714 + [[package]] 715 + name = "headers" 716 + version = "0.3.2" 717 + source = "registry+https://github.com/rust-lang/crates.io-index" 718 + checksum = "ed18eb2459bf1a09ad2d6b1547840c3e5e62882fa09b9a6a20b1de8e3228848f" 719 + dependencies = [ 720 + "base64 0.12.3", 721 + "bitflags", 722 + "bytes", 723 + "headers-core", 724 + "http", 725 + "mime 0.3.16", 726 + "sha-1", 727 + "time", 728 + ] 729 + 730 + [[package]] 731 + name = "headers-core" 732 + version = "0.2.0" 733 + source = "registry+https://github.com/rust-lang/crates.io-index" 734 + checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 735 + dependencies = [ 736 + "http", 737 + ] 738 + 739 + [[package]] 740 + name = "hermit-abi" 741 + version = "0.1.15" 742 + source = "registry+https://github.com/rust-lang/crates.io-index" 743 + checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" 744 + dependencies = [ 745 + "libc", 746 + ] 747 + 748 + [[package]] 749 + name = "hex" 750 + version = "0.4.2" 751 + source = "registry+https://github.com/rust-lang/crates.io-index" 752 + checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" 753 + 754 + [[package]] 755 + name = "http" 756 + version = "0.2.1" 757 + source = "registry+https://github.com/rust-lang/crates.io-index" 758 + checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" 759 + dependencies = [ 760 + "bytes", 761 + "fnv", 762 + "itoa", 763 + ] 764 + 765 + [[package]] 766 + name = "http-body" 767 + version = "0.3.1" 768 + source = "registry+https://github.com/rust-lang/crates.io-index" 769 + checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" 770 + dependencies = [ 771 + "bytes", 772 + "http", 773 + ] 774 + 775 + [[package]] 776 + name = "httparse" 777 + version = "1.3.4" 778 + source = "registry+https://github.com/rust-lang/crates.io-index" 779 + checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" 780 + 781 + [[package]] 782 + name = "humantime" 783 + version = "1.3.0" 784 + source = "registry+https://github.com/rust-lang/crates.io-index" 785 + checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 786 + dependencies = [ 787 + "quick-error", 788 + ] 789 + 790 + [[package]] 791 + name = "hyper" 792 + version = "0.13.7" 793 + source = "registry+https://github.com/rust-lang/crates.io-index" 794 + checksum = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" 795 + dependencies = [ 796 + "bytes", 797 + "futures-channel", 798 + "futures-core", 799 + "futures-util", 800 + "h2", 801 + "http", 802 + "http-body", 803 + "httparse", 804 + "itoa", 805 + "pin-project", 806 + "socket2", 807 + "time", 808 + "tokio", 809 + "tower-service", 810 + "tracing", 811 + "want", 812 + ] 813 + 814 + [[package]] 815 + name = "hyper-tls" 816 + version = "0.4.3" 817 + source = "registry+https://github.com/rust-lang/crates.io-index" 818 + checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" 819 + dependencies = [ 820 + "bytes", 821 + "hyper", 822 + "native-tls", 823 + "tokio", 824 + "tokio-tls", 825 + ] 826 + 827 + [[package]] 828 + name = "ident_case" 829 + version = "1.0.1" 830 + source = "registry+https://github.com/rust-lang/crates.io-index" 831 + checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 832 + 833 + [[package]] 834 + name = "idna" 835 + version = "0.2.0" 836 + source = "registry+https://github.com/rust-lang/crates.io-index" 837 + checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" 838 + dependencies = [ 839 + "matches", 840 + "unicode-bidi", 841 + "unicode-normalization", 842 + ] 843 + 844 + [[package]] 845 + name = "indexmap" 846 + version = "1.4.0" 847 + source = "registry+https://github.com/rust-lang/crates.io-index" 848 + checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe" 849 + dependencies = [ 850 + "autocfg 1.0.0", 851 + ] 852 + 853 + [[package]] 854 + name = "input_buffer" 855 + version = "0.3.1" 856 + source = "registry+https://github.com/rust-lang/crates.io-index" 857 + checksum = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754" 858 + dependencies = [ 859 + "bytes", 860 + ] 861 + 862 + [[package]] 863 + name = "iovec" 864 + version = "0.1.4" 865 + source = "registry+https://github.com/rust-lang/crates.io-index" 866 + checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 867 + dependencies = [ 868 + "libc", 869 + ] 870 + 871 + [[package]] 872 + name = "itertools" 873 + version = "0.9.0" 874 + source = "registry+https://github.com/rust-lang/crates.io-index" 875 + checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" 876 + dependencies = [ 877 + "either", 878 + ] 879 + 880 + [[package]] 881 + name = "itoa" 882 + version = "0.4.6" 883 + source = "registry+https://github.com/rust-lang/crates.io-index" 884 + checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" 885 + 886 + [[package]] 887 + name = "js-sys" 888 + version = "0.3.41" 889 + source = "registry+https://github.com/rust-lang/crates.io-index" 890 + checksum = "c4b9172132a62451e56142bff9afc91c8e4a4500aa5b847da36815b63bfda916" 891 + dependencies = [ 892 + "wasm-bindgen", 893 + ] 894 + 895 + [[package]] 896 + name = "jsonfeed" 897 + version = "0.3.0" 898 + dependencies = [ 899 + "error-chain", 900 + "serde", 901 + "serde_derive", 902 + "serde_json", 903 + ] 904 + 905 + [[package]] 906 + name = "kankyo" 907 + version = "0.3.0" 908 + source = "registry+https://github.com/rust-lang/crates.io-index" 909 + checksum = "325a11231fa70c1d1b562655db757cefb6022876d62f173831f35bd670ae0c40" 910 + 911 + [[package]] 912 + name = "kernel32-sys" 913 + version = "0.2.2" 914 + source = "registry+https://github.com/rust-lang/crates.io-index" 915 + checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 916 + dependencies = [ 917 + "winapi 0.2.8", 918 + "winapi-build", 919 + ] 920 + 921 + [[package]] 922 + name = "lazy_static" 923 + version = "1.4.0" 924 + source = "registry+https://github.com/rust-lang/crates.io-index" 925 + checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 926 + 927 + [[package]] 928 + name = "lexical-core" 929 + version = "0.7.4" 930 + source = "registry+https://github.com/rust-lang/crates.io-index" 931 + checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" 932 + dependencies = [ 933 + "arrayvec", 934 + "bitflags", 935 + "cfg-if", 936 + "ryu", 937 + "static_assertions", 938 + ] 939 + 940 + [[package]] 941 + name = "libc" 942 + version = "0.2.72" 943 + source = "registry+https://github.com/rust-lang/crates.io-index" 944 + checksum = "a9f8082297d534141b30c8d39e9b1773713ab50fdbe4ff30f750d063b3bfd701" 945 + 946 + [[package]] 947 + name = "libflate" 948 + version = "1.0.2" 949 + source = "registry+https://github.com/rust-lang/crates.io-index" 950 + checksum = "e9bac9023e1db29c084f9f8cd9d3852e5e8fddf98fb47c4964a0ea4663d95949" 951 + dependencies = [ 952 + "adler32", 953 + "crc32fast", 954 + "libflate_lz77", 955 + "rle-decode-fast", 956 + ] 957 + 958 + [[package]] 959 + name = "libflate_lz77" 960 + version = "1.0.0" 961 + source = "registry+https://github.com/rust-lang/crates.io-index" 962 + checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" 963 + 964 + [[package]] 965 + name = "linked-hash-map" 966 + version = "0.5.3" 967 + source = "registry+https://github.com/rust-lang/crates.io-index" 968 + checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" 969 + 970 + [[package]] 971 + name = "log" 972 + version = "0.3.9" 973 + source = "registry+https://github.com/rust-lang/crates.io-index" 974 + checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" 975 + dependencies = [ 976 + "log 0.4.8", 977 + ] 978 + 979 + [[package]] 980 + name = "log" 981 + version = "0.4.8" 982 + source = "registry+https://github.com/rust-lang/crates.io-index" 983 + checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 984 + dependencies = [ 985 + "cfg-if", 986 + ] 987 + 988 + [[package]] 989 + name = "maplit" 990 + version = "1.0.2" 991 + source = "registry+https://github.com/rust-lang/crates.io-index" 992 + checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 993 + 994 + [[package]] 995 + name = "matches" 996 + version = "0.1.8" 997 + source = "registry+https://github.com/rust-lang/crates.io-index" 998 + checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 999 + 1000 + [[package]] 1001 + name = "md5" 1002 + version = "0.7.0" 1003 + source = "registry+https://github.com/rust-lang/crates.io-index" 1004 + checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 1005 + 1006 + [[package]] 1007 + name = "memchr" 1008 + version = "2.3.3" 1009 + source = "registry+https://github.com/rust-lang/crates.io-index" 1010 + checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 1011 + 1012 + [[package]] 1013 + name = "mime" 1014 + version = "0.2.6" 1015 + source = "registry+https://github.com/rust-lang/crates.io-index" 1016 + checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" 1017 + dependencies = [ 1018 + "log 0.3.9", 1019 + ] 1020 + 1021 + [[package]] 1022 + name = "mime" 1023 + version = "0.3.16" 1024 + source = "registry+https://github.com/rust-lang/crates.io-index" 1025 + checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 1026 + 1027 + [[package]] 1028 + name = "mime_guess" 1029 + version = "1.8.8" 1030 + source = "registry+https://github.com/rust-lang/crates.io-index" 1031 + checksum = "216929a5ee4dd316b1702eedf5e74548c123d370f47841ceaac38ca154690ca3" 1032 + dependencies = [ 1033 + "mime 0.2.6", 1034 + "phf", 1035 + "phf_codegen", 1036 + "unicase 1.4.2", 1037 + ] 1038 + 1039 + [[package]] 1040 + name = "mime_guess" 1041 + version = "2.0.3" 1042 + source = "registry+https://github.com/rust-lang/crates.io-index" 1043 + checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" 1044 + dependencies = [ 1045 + "mime 0.3.16", 1046 + "unicase 2.6.0", 1047 + ] 1048 + 1049 + [[package]] 1050 + name = "miniz_oxide" 1051 + version = "0.4.0" 1052 + source = "registry+https://github.com/rust-lang/crates.io-index" 1053 + checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f" 1054 + dependencies = [ 1055 + "adler", 1056 + ] 1057 + 1058 + [[package]] 1059 + name = "mio" 1060 + version = "0.6.22" 1061 + source = "registry+https://github.com/rust-lang/crates.io-index" 1062 + checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" 1063 + dependencies = [ 1064 + "cfg-if", 1065 + "fuchsia-zircon", 1066 + "fuchsia-zircon-sys", 1067 + "iovec", 1068 + "kernel32-sys", 1069 + "libc", 1070 + "log 0.4.8", 1071 + "miow", 1072 + "net2", 1073 + "slab", 1074 + "winapi 0.2.8", 1075 + ] 1076 + 1077 + [[package]] 1078 + name = "miow" 1079 + version = "0.2.1" 1080 + source = "registry+https://github.com/rust-lang/crates.io-index" 1081 + checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" 1082 + dependencies = [ 1083 + "kernel32-sys", 1084 + "net2", 1085 + "winapi 0.2.8", 1086 + "ws2_32-sys", 1087 + ] 1088 + 1089 + [[package]] 1090 + name = "multipart" 1091 + version = "0.16.1" 1092 + source = "registry+https://github.com/rust-lang/crates.io-index" 1093 + checksum = "136eed74cadb9edd2651ffba732b19a450316b680e4f48d6c79e905799e19d01" 1094 + dependencies = [ 1095 + "buf_redux", 1096 + "httparse", 1097 + "log 0.4.8", 1098 + "mime 0.2.6", 1099 + "mime_guess 1.8.8", 1100 + "quick-error", 1101 + "rand 0.6.5", 1102 + "safemem", 1103 + "tempfile", 1104 + "twoway 0.1.8", 1105 + ] 1106 + 1107 + [[package]] 1108 + name = "native-tls" 1109 + version = "0.2.4" 1110 + source = "registry+https://github.com/rust-lang/crates.io-index" 1111 + checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" 1112 + dependencies = [ 1113 + "lazy_static", 1114 + "libc", 1115 + "log 0.4.8", 1116 + "openssl", 1117 + "openssl-probe", 1118 + "openssl-sys", 1119 + "schannel", 1120 + "security-framework", 1121 + "security-framework-sys", 1122 + "tempfile", 1123 + ] 1124 + 1125 + [[package]] 1126 + name = "net2" 1127 + version = "0.2.34" 1128 + source = "registry+https://github.com/rust-lang/crates.io-index" 1129 + checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" 1130 + dependencies = [ 1131 + "cfg-if", 1132 + "libc", 1133 + "winapi 0.3.9", 1134 + ] 1135 + 1136 + [[package]] 1137 + name = "nom" 1138 + version = "5.1.2" 1139 + source = "registry+https://github.com/rust-lang/crates.io-index" 1140 + checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" 1141 + dependencies = [ 1142 + "lexical-core", 1143 + "memchr", 1144 + "version_check 0.9.2", 1145 + ] 1146 + 1147 + [[package]] 1148 + name = "num-integer" 1149 + version = "0.1.43" 1150 + source = "registry+https://github.com/rust-lang/crates.io-index" 1151 + checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" 1152 + dependencies = [ 1153 + "autocfg 1.0.0", 1154 + "num-traits", 1155 + ] 1156 + 1157 + [[package]] 1158 + name = "num-traits" 1159 + version = "0.2.12" 1160 + source = "registry+https://github.com/rust-lang/crates.io-index" 1161 + checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" 1162 + dependencies = [ 1163 + "autocfg 1.0.0", 1164 + ] 1165 + 1166 + [[package]] 1167 + name = "num_cpus" 1168 + version = "1.13.0" 1169 + source = "registry+https://github.com/rust-lang/crates.io-index" 1170 + checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 1171 + dependencies = [ 1172 + "hermit-abi", 1173 + "libc", 1174 + ] 1175 + 1176 + [[package]] 1177 + name = "object" 1178 + version = "0.20.0" 1179 + source = "registry+https://github.com/rust-lang/crates.io-index" 1180 + checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" 1181 + 1182 + [[package]] 1183 + name = "once_cell" 1184 + version = "1.4.0" 1185 + source = "registry+https://github.com/rust-lang/crates.io-index" 1186 + checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" 1187 + 1188 + [[package]] 1189 + name = "opaque-debug" 1190 + version = "0.2.3" 1191 + source = "registry+https://github.com/rust-lang/crates.io-index" 1192 + checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" 1193 + 1194 + [[package]] 1195 + name = "openssl" 1196 + version = "0.10.30" 1197 + source = "registry+https://github.com/rust-lang/crates.io-index" 1198 + checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" 1199 + dependencies = [ 1200 + "bitflags", 1201 + "cfg-if", 1202 + "foreign-types", 1203 + "lazy_static", 1204 + "libc", 1205 + "openssl-sys", 1206 + ] 1207 + 1208 + [[package]] 1209 + name = "openssl-probe" 1210 + version = "0.1.2" 1211 + source = "registry+https://github.com/rust-lang/crates.io-index" 1212 + checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 1213 + 1214 + [[package]] 1215 + name = "openssl-sys" 1216 + version = "0.9.58" 1217 + source = "registry+https://github.com/rust-lang/crates.io-index" 1218 + checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" 1219 + dependencies = [ 1220 + "autocfg 1.0.0", 1221 + "cc", 1222 + "libc", 1223 + "pkg-config", 1224 + "vcpkg", 1225 + ] 1226 + 1227 + [[package]] 1228 + name = "parking" 1229 + version = "1.0.4" 1230 + source = "registry+https://github.com/rust-lang/crates.io-index" 1231 + checksum = "1efcee3c6d23b94012e240525f131c6abaa9e5eeb8f211002d93beec3b7be350" 1232 + 1233 + [[package]] 1234 + name = "patreon" 1235 + version = "0.1.0" 1236 + dependencies = [ 1237 + "chrono", 1238 + "envy", 1239 + "log 0.3.9", 1240 + "pretty_env_logger", 1241 + "reqwest", 1242 + "serde", 1243 + "serde_json", 1244 + "thiserror", 1245 + "tokio", 1246 + ] 1247 + 1248 + [[package]] 1249 + name = "percent-encoding" 1250 + version = "2.1.0" 1251 + source = "registry+https://github.com/rust-lang/crates.io-index" 1252 + checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 1253 + 1254 + [[package]] 1255 + name = "pest" 1256 + version = "2.1.3" 1257 + source = "registry+https://github.com/rust-lang/crates.io-index" 1258 + checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" 1259 + dependencies = [ 1260 + "ucd-trie", 1261 + ] 1262 + 1263 + [[package]] 1264 + name = "pest_consume" 1265 + version = "1.0.5" 1266 + source = "registry+https://github.com/rust-lang/crates.io-index" 1267 + checksum = "25f219b98d6adeb806008406459357c7692f413e2dd862219e262858d70a4108" 1268 + dependencies = [ 1269 + "pest", 1270 + "pest_consume_macros", 1271 + "pest_derive", 1272 + "proc-macro-hack", 1273 + ] 1274 + 1275 + [[package]] 1276 + name = "pest_consume_macros" 1277 + version = "1.0.5" 1278 + source = "registry+https://github.com/rust-lang/crates.io-index" 1279 + checksum = "4211c86227964037a5e04d55650bfd4392e7072539fa8cbc5f9ff47e77c22b4e" 1280 + dependencies = [ 1281 + "proc-macro-hack", 1282 + "proc-macro2", 1283 + "quote", 1284 + "syn", 1285 + ] 1286 + 1287 + [[package]] 1288 + name = "pest_derive" 1289 + version = "2.1.0" 1290 + source = "registry+https://github.com/rust-lang/crates.io-index" 1291 + checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" 1292 + dependencies = [ 1293 + "pest", 1294 + "pest_generator", 1295 + ] 1296 + 1297 + [[package]] 1298 + name = "pest_generator" 1299 + version = "2.1.3" 1300 + source = "registry+https://github.com/rust-lang/crates.io-index" 1301 + checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" 1302 + dependencies = [ 1303 + "pest", 1304 + "pest_meta", 1305 + "proc-macro2", 1306 + "quote", 1307 + "syn", 1308 + ] 1309 + 1310 + [[package]] 1311 + name = "pest_meta" 1312 + version = "2.1.3" 1313 + source = "registry+https://github.com/rust-lang/crates.io-index" 1314 + checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" 1315 + dependencies = [ 1316 + "maplit", 1317 + "pest", 1318 + "sha-1", 1319 + ] 1320 + 1321 + [[package]] 1322 + name = "phf" 1323 + version = "0.7.24" 1324 + source = "registry+https://github.com/rust-lang/crates.io-index" 1325 + checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" 1326 + dependencies = [ 1327 + "phf_shared", 1328 + ] 1329 + 1330 + [[package]] 1331 + name = "phf_codegen" 1332 + version = "0.7.24" 1333 + source = "registry+https://github.com/rust-lang/crates.io-index" 1334 + checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" 1335 + dependencies = [ 1336 + "phf_generator", 1337 + "phf_shared", 1338 + ] 1339 + 1340 + [[package]] 1341 + name = "phf_generator" 1342 + version = "0.7.24" 1343 + source = "registry+https://github.com/rust-lang/crates.io-index" 1344 + checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" 1345 + dependencies = [ 1346 + "phf_shared", 1347 + "rand 0.6.5", 1348 + ] 1349 + 1350 + [[package]] 1351 + name = "phf_shared" 1352 + version = "0.7.24" 1353 + source = "registry+https://github.com/rust-lang/crates.io-index" 1354 + checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" 1355 + dependencies = [ 1356 + "siphasher", 1357 + "unicase 1.4.2", 1358 + ] 1359 + 1360 + [[package]] 1361 + name = "pin-project" 1362 + version = "0.4.22" 1363 + source = "registry+https://github.com/rust-lang/crates.io-index" 1364 + checksum = "12e3a6cdbfe94a5e4572812a0201f8c0ed98c1c452c7b8563ce2276988ef9c17" 1365 + dependencies = [ 1366 + "pin-project-internal", 1367 + ] 1368 + 1369 + [[package]] 1370 + name = "pin-project-internal" 1371 + version = "0.4.22" 1372 + source = "registry+https://github.com/rust-lang/crates.io-index" 1373 + checksum = "6a0ffd45cf79d88737d7cc85bfd5d2894bee1139b356e616fe85dc389c61aaf7" 1374 + dependencies = [ 1375 + "proc-macro2", 1376 + "quote", 1377 + "syn", 1378 + ] 1379 + 1380 + [[package]] 1381 + name = "pin-project-lite" 1382 + version = "0.1.7" 1383 + source = "registry+https://github.com/rust-lang/crates.io-index" 1384 + checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" 1385 + 1386 + [[package]] 1387 + name = "pin-utils" 1388 + version = "0.1.0" 1389 + source = "registry+https://github.com/rust-lang/crates.io-index" 1390 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1391 + 1392 + [[package]] 1393 + name = "pkg-config" 1394 + version = "0.3.18" 1395 + source = "registry+https://github.com/rust-lang/crates.io-index" 1396 + checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" 1397 + 1398 + [[package]] 1399 + name = "ppv-lite86" 1400 + version = "0.2.8" 1401 + source = "registry+https://github.com/rust-lang/crates.io-index" 1402 + checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" 1403 + 1404 + [[package]] 1405 + name = "pretty" 1406 + version = "0.5.2" 1407 + source = "registry+https://github.com/rust-lang/crates.io-index" 1408 + checksum = "f60c0d9f6fc88ecdd245d90c1920ff76a430ab34303fc778d33b1d0a4c3bf6d3" 1409 + dependencies = [ 1410 + "typed-arena", 1411 + ] 1412 + 1413 + [[package]] 1414 + name = "pretty_env_logger" 1415 + version = "0.4.0" 1416 + source = "registry+https://github.com/rust-lang/crates.io-index" 1417 + checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" 1418 + dependencies = [ 1419 + "env_logger", 1420 + "log 0.4.8", 1421 + ] 1422 + 1423 + [[package]] 1424 + name = "proc-macro-hack" 1425 + version = "0.5.16" 1426 + source = "registry+https://github.com/rust-lang/crates.io-index" 1427 + checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" 1428 + 1429 + [[package]] 1430 + name = "proc-macro-nested" 1431 + version = "0.1.6" 1432 + source = "registry+https://github.com/rust-lang/crates.io-index" 1433 + checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" 1434 + 1435 + [[package]] 1436 + name = "proc-macro2" 1437 + version = "1.0.18" 1438 + source = "registry+https://github.com/rust-lang/crates.io-index" 1439 + checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" 1440 + dependencies = [ 1441 + "unicode-xid", 1442 + ] 1443 + 1444 + [[package]] 1445 + name = "procfs" 1446 + version = "0.7.9" 1447 + source = "registry+https://github.com/rust-lang/crates.io-index" 1448 + checksum = "c434e93ef69c216e68e4f417c927b4f31502c3560b72cfdb6827e2321c5c6b3e" 1449 + dependencies = [ 1450 + "bitflags", 1451 + "byteorder", 1452 + "hex", 1453 + "lazy_static", 1454 + "libc", 1455 + "libflate", 1456 + ] 1457 + 1458 + [[package]] 1459 + name = "prometheus" 1460 + version = "0.9.0" 1461 + source = "registry+https://github.com/rust-lang/crates.io-index" 1462 + checksum = "dd0ced56dee39a6e960c15c74dc48849d614586db2eaada6497477af7c7811cd" 1463 + dependencies = [ 1464 + "cfg-if", 1465 + "fnv", 1466 + "lazy_static", 1467 + "libc", 1468 + "procfs", 1469 + "spin", 1470 + "thiserror", 1471 + ] 1472 + 1473 + [[package]] 1474 + name = "quick-error" 1475 + version = "1.2.3" 1476 + source = "registry+https://github.com/rust-lang/crates.io-index" 1477 + checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 1478 + 1479 + [[package]] 1480 + name = "quick-xml" 1481 + version = "0.17.2" 1482 + source = "registry+https://github.com/rust-lang/crates.io-index" 1483 + checksum = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" 1484 + dependencies = [ 1485 + "encoding_rs", 1486 + "memchr", 1487 + ] 1488 + 1489 + [[package]] 1490 + name = "quick-xml" 1491 + version = "0.18.1" 1492 + source = "registry+https://github.com/rust-lang/crates.io-index" 1493 + checksum = "3cc440ee4802a86e357165021e3e255a9143724da31db1e2ea540214c96a0f82" 1494 + dependencies = [ 1495 + "encoding_rs", 1496 + "memchr", 1497 + ] 1498 + 1499 + [[package]] 1500 + name = "quote" 1501 + version = "1.0.7" 1502 + source = "registry+https://github.com/rust-lang/crates.io-index" 1503 + checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 1504 + dependencies = [ 1505 + "proc-macro2", 1506 + ] 1507 + 1508 + [[package]] 1509 + name = "rand" 1510 + version = "0.6.5" 1511 + source = "registry+https://github.com/rust-lang/crates.io-index" 1512 + checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" 1513 + dependencies = [ 1514 + "autocfg 0.1.7", 1515 + "libc", 1516 + "rand_chacha 0.1.1", 1517 + "rand_core 0.4.2", 1518 + "rand_hc 0.1.0", 1519 + "rand_isaac", 1520 + "rand_jitter", 1521 + "rand_os", 1522 + "rand_pcg", 1523 + "rand_xorshift", 1524 + "winapi 0.3.9", 1525 + ] 1526 + 1527 + [[package]] 1528 + name = "rand" 1529 + version = "0.7.3" 1530 + source = "registry+https://github.com/rust-lang/crates.io-index" 1531 + checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 1532 + dependencies = [ 1533 + "getrandom", 1534 + "libc", 1535 + "rand_chacha 0.2.2", 1536 + "rand_core 0.5.1", 1537 + "rand_hc 0.2.0", 1538 + ] 1539 + 1540 + [[package]] 1541 + name = "rand_chacha" 1542 + version = "0.1.1" 1543 + source = "registry+https://github.com/rust-lang/crates.io-index" 1544 + checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" 1545 + dependencies = [ 1546 + "autocfg 0.1.7", 1547 + "rand_core 0.3.1", 1548 + ] 1549 + 1550 + [[package]] 1551 + name = "rand_chacha" 1552 + version = "0.2.2" 1553 + source = "registry+https://github.com/rust-lang/crates.io-index" 1554 + checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 1555 + dependencies = [ 1556 + "ppv-lite86", 1557 + "rand_core 0.5.1", 1558 + ] 1559 + 1560 + [[package]] 1561 + name = "rand_core" 1562 + version = "0.3.1" 1563 + source = "registry+https://github.com/rust-lang/crates.io-index" 1564 + checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 1565 + dependencies = [ 1566 + "rand_core 0.4.2", 1567 + ] 1568 + 1569 + [[package]] 1570 + name = "rand_core" 1571 + version = "0.4.2" 1572 + source = "registry+https://github.com/rust-lang/crates.io-index" 1573 + checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 1574 + 1575 + [[package]] 1576 + name = "rand_core" 1577 + version = "0.5.1" 1578 + source = "registry+https://github.com/rust-lang/crates.io-index" 1579 + checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 1580 + dependencies = [ 1581 + "getrandom", 1582 + ] 1583 + 1584 + [[package]] 1585 + name = "rand_hc" 1586 + version = "0.1.0" 1587 + source = "registry+https://github.com/rust-lang/crates.io-index" 1588 + checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" 1589 + dependencies = [ 1590 + "rand_core 0.3.1", 1591 + ] 1592 + 1593 + [[package]] 1594 + name = "rand_hc" 1595 + version = "0.2.0" 1596 + source = "registry+https://github.com/rust-lang/crates.io-index" 1597 + checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 1598 + dependencies = [ 1599 + "rand_core 0.5.1", 1600 + ] 1601 + 1602 + [[package]] 1603 + name = "rand_isaac" 1604 + version = "0.1.1" 1605 + source = "registry+https://github.com/rust-lang/crates.io-index" 1606 + checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" 1607 + dependencies = [ 1608 + "rand_core 0.3.1", 1609 + ] 1610 + 1611 + [[package]] 1612 + name = "rand_jitter" 1613 + version = "0.1.4" 1614 + source = "registry+https://github.com/rust-lang/crates.io-index" 1615 + checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" 1616 + dependencies = [ 1617 + "libc", 1618 + "rand_core 0.4.2", 1619 + "winapi 0.3.9", 1620 + ] 1621 + 1622 + [[package]] 1623 + name = "rand_os" 1624 + version = "0.1.3" 1625 + source = "registry+https://github.com/rust-lang/crates.io-index" 1626 + checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 1627 + dependencies = [ 1628 + "cloudabi", 1629 + "fuchsia-cprng", 1630 + "libc", 1631 + "rand_core 0.4.2", 1632 + "rdrand", 1633 + "winapi 0.3.9", 1634 + ] 1635 + 1636 + [[package]] 1637 + name = "rand_pcg" 1638 + version = "0.1.2" 1639 + source = "registry+https://github.com/rust-lang/crates.io-index" 1640 + checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" 1641 + dependencies = [ 1642 + "autocfg 0.1.7", 1643 + "rand_core 0.4.2", 1644 + ] 1645 + 1646 + [[package]] 1647 + name = "rand_xorshift" 1648 + version = "0.1.1" 1649 + source = "registry+https://github.com/rust-lang/crates.io-index" 1650 + checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" 1651 + dependencies = [ 1652 + "rand_core 0.3.1", 1653 + ] 1654 + 1655 + [[package]] 1656 + name = "rdrand" 1657 + version = "0.4.0" 1658 + source = "registry+https://github.com/rust-lang/crates.io-index" 1659 + checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 1660 + dependencies = [ 1661 + "rand_core 0.3.1", 1662 + ] 1663 + 1664 + [[package]] 1665 + name = "redox_syscall" 1666 + version = "0.1.57" 1667 + source = "registry+https://github.com/rust-lang/crates.io-index" 1668 + checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 1669 + 1670 + [[package]] 1671 + name = "regex" 1672 + version = "1.3.9" 1673 + source = "registry+https://github.com/rust-lang/crates.io-index" 1674 + checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 1675 + dependencies = [ 1676 + "aho-corasick", 1677 + "memchr", 1678 + "regex-syntax", 1679 + "thread_local", 1680 + ] 1681 + 1682 + [[package]] 1683 + name = "regex-syntax" 1684 + version = "0.6.18" 1685 + source = "registry+https://github.com/rust-lang/crates.io-index" 1686 + checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 1687 + 1688 + [[package]] 1689 + name = "remove_dir_all" 1690 + version = "0.5.3" 1691 + source = "registry+https://github.com/rust-lang/crates.io-index" 1692 + checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1693 + dependencies = [ 1694 + "winapi 0.3.9", 1695 + ] 1696 + 1697 + [[package]] 1698 + name = "reqwest" 1699 + version = "0.10.6" 1700 + source = "registry+https://github.com/rust-lang/crates.io-index" 1701 + checksum = "3b82c9238b305f26f53443e3a4bc8528d64b8d0bee408ec949eb7bf5635ec680" 1702 + dependencies = [ 1703 + "base64 0.12.3", 1704 + "bytes", 1705 + "encoding_rs", 1706 + "futures-core", 1707 + "futures-util", 1708 + "http", 1709 + "http-body", 1710 + "hyper", 1711 + "hyper-tls", 1712 + "js-sys", 1713 + "lazy_static", 1714 + "log 0.4.8", 1715 + "mime 0.3.16", 1716 + "mime_guess 2.0.3", 1717 + "native-tls", 1718 + "percent-encoding", 1719 + "pin-project-lite", 1720 + "serde", 1721 + "serde_json", 1722 + "serde_urlencoded", 1723 + "tokio", 1724 + "tokio-tls", 1725 + "url", 1726 + "wasm-bindgen", 1727 + "wasm-bindgen-futures", 1728 + "web-sys", 1729 + "winreg", 1730 + ] 1731 + 1732 + [[package]] 1733 + name = "rle-decode-fast" 1734 + version = "1.0.1" 1735 + source = "registry+https://github.com/rust-lang/crates.io-index" 1736 + checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" 1737 + 1738 + [[package]] 1739 + name = "rss" 1740 + version = "1.9.0" 1741 + source = "registry+https://github.com/rust-lang/crates.io-index" 1742 + checksum = "99979205510c60f80a119dedbabd0b8426517384edf205322f8bcd51796bcef9" 1743 + dependencies = [ 1744 + "derive_builder", 1745 + "quick-xml 0.17.2", 1746 + ] 1747 + 1748 + [[package]] 1749 + name = "ructe" 1750 + version = "0.11.4" 1751 + source = "registry+https://github.com/rust-lang/crates.io-index" 1752 + checksum = "f615d1e172dcc01a7cd78c7f77f21a5669c6de4341548ad2e7764e9045d06657" 1753 + dependencies = [ 1754 + "base64 0.12.3", 1755 + "bytecount", 1756 + "itertools", 1757 + "md5", 1758 + "mime 0.3.16", 1759 + "nom", 1760 + ] 1761 + 1762 + [[package]] 1763 + name = "rustc-demangle" 1764 + version = "0.1.16" 1765 + source = "registry+https://github.com/rust-lang/crates.io-index" 1766 + checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" 1767 + 1768 + [[package]] 1769 + name = "ryu" 1770 + version = "1.0.5" 1771 + source = "registry+https://github.com/rust-lang/crates.io-index" 1772 + checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 1773 + 1774 + [[package]] 1775 + name = "safemem" 1776 + version = "0.3.3" 1777 + source = "registry+https://github.com/rust-lang/crates.io-index" 1778 + checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 1779 + 1780 + [[package]] 1781 + name = "same-file" 1782 + version = "1.0.6" 1783 + source = "registry+https://github.com/rust-lang/crates.io-index" 1784 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1785 + dependencies = [ 1786 + "winapi-util", 1787 + ] 1788 + 1789 + [[package]] 1790 + name = "schannel" 1791 + version = "0.1.19" 1792 + source = "registry+https://github.com/rust-lang/crates.io-index" 1793 + checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" 1794 + dependencies = [ 1795 + "lazy_static", 1796 + "winapi 0.3.9", 1797 + ] 1798 + 1799 + [[package]] 1800 + name = "scoped-tls" 1801 + version = "1.0.0" 1802 + source = "registry+https://github.com/rust-lang/crates.io-index" 1803 + checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" 1804 + 1805 + [[package]] 1806 + name = "security-framework" 1807 + version = "0.4.4" 1808 + source = "registry+https://github.com/rust-lang/crates.io-index" 1809 + checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" 1810 + dependencies = [ 1811 + "bitflags", 1812 + "core-foundation", 1813 + "core-foundation-sys", 1814 + "libc", 1815 + "security-framework-sys", 1816 + ] 1817 + 1818 + [[package]] 1819 + name = "security-framework-sys" 1820 + version = "0.4.3" 1821 + source = "registry+https://github.com/rust-lang/crates.io-index" 1822 + checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" 1823 + dependencies = [ 1824 + "core-foundation-sys", 1825 + "libc", 1826 + ] 1827 + 1828 + [[package]] 1829 + name = "serde" 1830 + version = "1.0.114" 1831 + source = "registry+https://github.com/rust-lang/crates.io-index" 1832 + checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" 1833 + dependencies = [ 1834 + "serde_derive", 1835 + ] 1836 + 1837 + [[package]] 1838 + name = "serde_cbor" 1839 + version = "0.9.0" 1840 + source = "registry+https://github.com/rust-lang/crates.io-index" 1841 + checksum = "45cd6d95391b16cd57e88b68be41d504183b7faae22030c0cc3b3f73dd57b2fd" 1842 + dependencies = [ 1843 + "byteorder", 1844 + "half", 1845 + "serde", 1846 + ] 1847 + 1848 + [[package]] 1849 + name = "serde_derive" 1850 + version = "1.0.114" 1851 + source = "registry+https://github.com/rust-lang/crates.io-index" 1852 + checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" 1853 + dependencies = [ 1854 + "proc-macro2", 1855 + "quote", 1856 + "syn", 1857 + ] 1858 + 1859 + [[package]] 1860 + name = "serde_dhall" 1861 + version = "0.5.3" 1862 + source = "registry+https://github.com/rust-lang/crates.io-index" 1863 + checksum = "cfec10f64aaaf9b296dbd4355ec4f2c3248621cd8b114851fbd7fda3bf4d9374" 1864 + dependencies = [ 1865 + "dhall", 1866 + "dhall_proc_macros", 1867 + "doc-comment", 1868 + "serde", 1869 + "url", 1870 + ] 1871 + 1872 + [[package]] 1873 + name = "serde_json" 1874 + version = "1.0.56" 1875 + source = "registry+https://github.com/rust-lang/crates.io-index" 1876 + checksum = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3" 1877 + dependencies = [ 1878 + "itoa", 1879 + "ryu", 1880 + "serde", 1881 + ] 1882 + 1883 + [[package]] 1884 + name = "serde_urlencoded" 1885 + version = "0.6.1" 1886 + source = "registry+https://github.com/rust-lang/crates.io-index" 1887 + checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" 1888 + dependencies = [ 1889 + "dtoa", 1890 + "itoa", 1891 + "serde", 1892 + "url", 1893 + ] 1894 + 1895 + [[package]] 1896 + name = "serde_yaml" 1897 + version = "0.8.13" 1898 + source = "registry+https://github.com/rust-lang/crates.io-index" 1899 + checksum = "ae3e2dd40a7cdc18ca80db804b7f461a39bb721160a85c9a1fa30134bf3c02a5" 1900 + dependencies = [ 1901 + "dtoa", 1902 + "linked-hash-map", 1903 + "serde", 1904 + "yaml-rust", 1905 + ] 1906 + 1907 + [[package]] 1908 + name = "sha-1" 1909 + version = "0.8.2" 1910 + source = "registry+https://github.com/rust-lang/crates.io-index" 1911 + checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" 1912 + dependencies = [ 1913 + "block-buffer", 1914 + "digest", 1915 + "fake-simd", 1916 + "opaque-debug", 1917 + ] 1918 + 1919 + [[package]] 1920 + name = "sha2" 1921 + version = "0.8.2" 1922 + source = "registry+https://github.com/rust-lang/crates.io-index" 1923 + checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" 1924 + dependencies = [ 1925 + "block-buffer", 1926 + "digest", 1927 + "fake-simd", 1928 + "opaque-debug", 1929 + ] 1930 + 1931 + [[package]] 1932 + name = "siphasher" 1933 + version = "0.2.3" 1934 + source = "registry+https://github.com/rust-lang/crates.io-index" 1935 + checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" 1936 + 1937 + [[package]] 1938 + name = "sitemap" 1939 + version = "0.4.0" 1940 + source = "registry+https://github.com/rust-lang/crates.io-index" 1941 + checksum = "5c95c7e58b4461fec85bdd58f271bcd416ecc4d630c3ac280b60efa3421016b7" 1942 + dependencies = [ 1943 + "chrono", 1944 + "chrono_utils", 1945 + "url", 1946 + "xml-rs", 1947 + ] 1948 + 1949 + [[package]] 1950 + name = "slab" 1951 + version = "0.4.2" 1952 + source = "registry+https://github.com/rust-lang/crates.io-index" 1953 + checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 1954 + 1955 + [[package]] 1956 + name = "smallvec" 1957 + version = "1.4.1" 1958 + source = "registry+https://github.com/rust-lang/crates.io-index" 1959 + checksum = "3757cb9d89161a2f24e1cf78efa0c1fcff485d18e3f55e0aa3480824ddaa0f3f" 1960 + 1961 + [[package]] 1962 + name = "socket2" 1963 + version = "0.3.12" 1964 + source = "registry+https://github.com/rust-lang/crates.io-index" 1965 + checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" 1966 + dependencies = [ 1967 + "cfg-if", 1968 + "libc", 1969 + "redox_syscall", 1970 + "winapi 0.3.9", 1971 + ] 1972 + 1973 + [[package]] 1974 + name = "spin" 1975 + version = "0.5.2" 1976 + source = "registry+https://github.com/rust-lang/crates.io-index" 1977 + checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1978 + 1979 + [[package]] 1980 + name = "static_assertions" 1981 + version = "1.1.0" 1982 + source = "registry+https://github.com/rust-lang/crates.io-index" 1983 + checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1984 + 1985 + [[package]] 1986 + name = "strsim" 1987 + version = "0.8.0" 1988 + source = "registry+https://github.com/rust-lang/crates.io-index" 1989 + checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1990 + 1991 + [[package]] 1992 + name = "strsim" 1993 + version = "0.9.3" 1994 + source = "registry+https://github.com/rust-lang/crates.io-index" 1995 + checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" 1996 + 1997 + [[package]] 1998 + name = "syn" 1999 + version = "1.0.34" 2000 + source = "registry+https://github.com/rust-lang/crates.io-index" 2001 + checksum = "936cae2873c940d92e697597c5eee105fb570cd5689c695806f672883653349b" 2002 + dependencies = [ 2003 + "proc-macro2", 2004 + "quote", 2005 + "unicode-xid", 2006 + ] 2007 + 2008 + [[package]] 2009 + name = "tempfile" 2010 + version = "3.1.0" 2011 + source = "registry+https://github.com/rust-lang/crates.io-index" 2012 + checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 2013 + dependencies = [ 2014 + "cfg-if", 2015 + "libc", 2016 + "rand 0.7.3", 2017 + "redox_syscall", 2018 + "remove_dir_all", 2019 + "winapi 0.3.9", 2020 + ] 2021 + 2022 + [[package]] 2023 + name = "termcolor" 2024 + version = "1.1.0" 2025 + source = "registry+https://github.com/rust-lang/crates.io-index" 2026 + checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 2027 + dependencies = [ 2028 + "winapi-util", 2029 + ] 2030 + 2031 + [[package]] 2032 + name = "textwrap" 2033 + version = "0.11.0" 2034 + source = "registry+https://github.com/rust-lang/crates.io-index" 2035 + checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 2036 + dependencies = [ 2037 + "unicode-width", 2038 + ] 2039 + 2040 + [[package]] 2041 + name = "thiserror" 2042 + version = "1.0.20" 2043 + source = "registry+https://github.com/rust-lang/crates.io-index" 2044 + checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" 2045 + dependencies = [ 2046 + "thiserror-impl", 2047 + ] 2048 + 2049 + [[package]] 2050 + name = "thiserror-impl" 2051 + version = "1.0.20" 2052 + source = "registry+https://github.com/rust-lang/crates.io-index" 2053 + checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" 2054 + dependencies = [ 2055 + "proc-macro2", 2056 + "quote", 2057 + "syn", 2058 + ] 2059 + 2060 + [[package]] 2061 + name = "thread_local" 2062 + version = "1.0.1" 2063 + source = "registry+https://github.com/rust-lang/crates.io-index" 2064 + checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 2065 + dependencies = [ 2066 + "lazy_static", 2067 + ] 2068 + 2069 + [[package]] 2070 + name = "time" 2071 + version = "0.1.43" 2072 + source = "registry+https://github.com/rust-lang/crates.io-index" 2073 + checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 2074 + dependencies = [ 2075 + "libc", 2076 + "winapi 0.3.9", 2077 + ] 2078 + 2079 + [[package]] 2080 + name = "tinyvec" 2081 + version = "0.3.3" 2082 + source = "registry+https://github.com/rust-lang/crates.io-index" 2083 + checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" 2084 + 2085 + [[package]] 2086 + name = "tokio" 2087 + version = "0.2.21" 2088 + source = "registry+https://github.com/rust-lang/crates.io-index" 2089 + checksum = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58" 2090 + dependencies = [ 2091 + "bytes", 2092 + "fnv", 2093 + "futures-core", 2094 + "iovec", 2095 + "lazy_static", 2096 + "memchr", 2097 + "mio", 2098 + "num_cpus", 2099 + "pin-project-lite", 2100 + "slab", 2101 + "tokio-macros", 2102 + ] 2103 + 2104 + [[package]] 2105 + name = "tokio-macros" 2106 + version = "0.2.5" 2107 + source = "registry+https://github.com/rust-lang/crates.io-index" 2108 + checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" 2109 + dependencies = [ 2110 + "proc-macro2", 2111 + "quote", 2112 + "syn", 2113 + ] 2114 + 2115 + [[package]] 2116 + name = "tokio-tls" 2117 + version = "0.3.1" 2118 + source = "registry+https://github.com/rust-lang/crates.io-index" 2119 + checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" 2120 + dependencies = [ 2121 + "native-tls", 2122 + "tokio", 2123 + ] 2124 + 2125 + [[package]] 2126 + name = "tokio-tungstenite" 2127 + version = "0.10.1" 2128 + source = "registry+https://github.com/rust-lang/crates.io-index" 2129 + checksum = "b8b8fe88007ebc363512449868d7da4389c9400072a3f666f212c7280082882a" 2130 + dependencies = [ 2131 + "futures", 2132 + "log 0.4.8", 2133 + "pin-project", 2134 + "tokio", 2135 + "tungstenite", 2136 + ] 2137 + 2138 + [[package]] 2139 + name = "tokio-util" 2140 + version = "0.3.1" 2141 + source = "registry+https://github.com/rust-lang/crates.io-index" 2142 + checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" 2143 + dependencies = [ 2144 + "bytes", 2145 + "futures-core", 2146 + "futures-sink", 2147 + "log 0.4.8", 2148 + "pin-project-lite", 2149 + "tokio", 2150 + ] 2151 + 2152 + [[package]] 2153 + name = "tower-service" 2154 + version = "0.3.0" 2155 + source = "registry+https://github.com/rust-lang/crates.io-index" 2156 + checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" 2157 + 2158 + [[package]] 2159 + name = "tracing" 2160 + version = "0.1.16" 2161 + source = "registry+https://github.com/rust-lang/crates.io-index" 2162 + checksum = "c2e2a2de6b0d5cbb13fc21193a2296888eaab62b6044479aafb3c54c01c29fcd" 2163 + dependencies = [ 2164 + "cfg-if", 2165 + "log 0.4.8", 2166 + "tracing-core", 2167 + ] 2168 + 2169 + [[package]] 2170 + name = "tracing-core" 2171 + version = "0.1.11" 2172 + source = "registry+https://github.com/rust-lang/crates.io-index" 2173 + checksum = "94ae75f0d28ae10786f3b1895c55fe72e79928fd5ccdebb5438c75e93fec178f" 2174 + dependencies = [ 2175 + "lazy_static", 2176 + ] 2177 + 2178 + [[package]] 2179 + name = "try-lock" 2180 + version = "0.2.3" 2181 + source = "registry+https://github.com/rust-lang/crates.io-index" 2182 + checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 2183 + 2184 + [[package]] 2185 + name = "tungstenite" 2186 + version = "0.10.1" 2187 + source = "registry+https://github.com/rust-lang/crates.io-index" 2188 + checksum = "cfea31758bf674f990918962e8e5f07071a3161bd7c4138ed23e416e1ac4264e" 2189 + dependencies = [ 2190 + "base64 0.11.0", 2191 + "byteorder", 2192 + "bytes", 2193 + "http", 2194 + "httparse", 2195 + "input_buffer", 2196 + "log 0.4.8", 2197 + "rand 0.7.3", 2198 + "sha-1", 2199 + "url", 2200 + "utf-8", 2201 + ] 2202 + 2203 + [[package]] 2204 + name = "twoway" 2205 + version = "0.1.8" 2206 + source = "registry+https://github.com/rust-lang/crates.io-index" 2207 + checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" 2208 + dependencies = [ 2209 + "memchr", 2210 + ] 2211 + 2212 + [[package]] 2213 + name = "twoway" 2214 + version = "0.2.1" 2215 + source = "registry+https://github.com/rust-lang/crates.io-index" 2216 + checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc" 2217 + dependencies = [ 2218 + "memchr", 2219 + "unchecked-index", 2220 + ] 2221 + 2222 + [[package]] 2223 + name = "typed-arena" 2224 + version = "1.7.0" 2225 + source = "registry+https://github.com/rust-lang/crates.io-index" 2226 + checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" 2227 + 2228 + [[package]] 2229 + name = "typenum" 2230 + version = "1.12.0" 2231 + source = "registry+https://github.com/rust-lang/crates.io-index" 2232 + checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" 2233 + 2234 + [[package]] 2235 + name = "ucd-trie" 2236 + version = "0.1.3" 2237 + source = "registry+https://github.com/rust-lang/crates.io-index" 2238 + checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" 2239 + 2240 + [[package]] 2241 + name = "unchecked-index" 2242 + version = "0.2.2" 2243 + source = "registry+https://github.com/rust-lang/crates.io-index" 2244 + checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" 2245 + 2246 + [[package]] 2247 + name = "unicase" 2248 + version = "1.4.2" 2249 + source = "registry+https://github.com/rust-lang/crates.io-index" 2250 + checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" 2251 + dependencies = [ 2252 + "version_check 0.1.5", 2253 + ] 2254 + 2255 + [[package]] 2256 + name = "unicase" 2257 + version = "2.6.0" 2258 + source = "registry+https://github.com/rust-lang/crates.io-index" 2259 + checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 2260 + dependencies = [ 2261 + "version_check 0.9.2", 2262 + ] 2263 + 2264 + [[package]] 2265 + name = "unicode-bidi" 2266 + version = "0.3.4" 2267 + source = "registry+https://github.com/rust-lang/crates.io-index" 2268 + checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 2269 + dependencies = [ 2270 + "matches", 2271 + ] 2272 + 2273 + [[package]] 2274 + name = "unicode-normalization" 2275 + version = "0.1.13" 2276 + source = "registry+https://github.com/rust-lang/crates.io-index" 2277 + checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" 2278 + dependencies = [ 2279 + "tinyvec", 2280 + ] 2281 + 2282 + [[package]] 2283 + name = "unicode-width" 2284 + version = "0.1.8" 2285 + source = "registry+https://github.com/rust-lang/crates.io-index" 2286 + checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 2287 + 2288 + [[package]] 2289 + name = "unicode-xid" 2290 + version = "0.2.1" 2291 + source = "registry+https://github.com/rust-lang/crates.io-index" 2292 + checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 2293 + 2294 + [[package]] 2295 + name = "unicode_categories" 2296 + version = "0.1.1" 2297 + source = "registry+https://github.com/rust-lang/crates.io-index" 2298 + checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 2299 + 2300 + [[package]] 2301 + name = "url" 2302 + version = "2.1.1" 2303 + source = "registry+https://github.com/rust-lang/crates.io-index" 2304 + checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" 2305 + dependencies = [ 2306 + "idna", 2307 + "matches", 2308 + "percent-encoding", 2309 + ] 2310 + 2311 + [[package]] 2312 + name = "urlencoding" 2313 + version = "1.1.1" 2314 + source = "registry+https://github.com/rust-lang/crates.io-index" 2315 + checksum = "c9232eb53352b4442e40d7900465dfc534e8cb2dc8f18656fcb2ac16112b5593" 2316 + 2317 + [[package]] 2318 + name = "utf-8" 2319 + version = "0.7.5" 2320 + source = "registry+https://github.com/rust-lang/crates.io-index" 2321 + checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" 2322 + 2323 + [[package]] 2324 + name = "vcpkg" 2325 + version = "0.2.10" 2326 + source = "registry+https://github.com/rust-lang/crates.io-index" 2327 + checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" 2328 + 2329 + [[package]] 2330 + name = "vec_map" 2331 + version = "0.8.2" 2332 + source = "registry+https://github.com/rust-lang/crates.io-index" 2333 + checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 2334 + 2335 + [[package]] 2336 + name = "version_check" 2337 + version = "0.1.5" 2338 + source = "registry+https://github.com/rust-lang/crates.io-index" 2339 + checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 2340 + 2341 + [[package]] 2342 + name = "version_check" 2343 + version = "0.9.2" 2344 + source = "registry+https://github.com/rust-lang/crates.io-index" 2345 + checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 2346 + 2347 + [[package]] 2348 + name = "waker-fn" 2349 + version = "1.0.0" 2350 + source = "registry+https://github.com/rust-lang/crates.io-index" 2351 + checksum = "9571542c2ce85ce642e6b58b3364da2fb53526360dfb7c211add4f5c23105ff7" 2352 + 2353 + [[package]] 2354 + name = "walkdir" 2355 + version = "2.3.1" 2356 + source = "registry+https://github.com/rust-lang/crates.io-index" 2357 + checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" 2358 + dependencies = [ 2359 + "same-file", 2360 + "winapi 0.3.9", 2361 + "winapi-util", 2362 + ] 2363 + 2364 + [[package]] 2365 + name = "want" 2366 + version = "0.3.0" 2367 + source = "registry+https://github.com/rust-lang/crates.io-index" 2368 + checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 2369 + dependencies = [ 2370 + "log 0.4.8", 2371 + "try-lock", 2372 + ] 2373 + 2374 + [[package]] 2375 + name = "warp" 2376 + version = "0.2.3" 2377 + source = "registry+https://github.com/rust-lang/crates.io-index" 2378 + checksum = "0e95175b7a927258ecbb816bdada3cc469cb68593e7940b96a60f4af366a9970" 2379 + dependencies = [ 2380 + "bytes", 2381 + "futures", 2382 + "headers", 2383 + "http", 2384 + "hyper", 2385 + "log 0.4.8", 2386 + "mime 0.3.16", 2387 + "mime_guess 2.0.3", 2388 + "multipart", 2389 + "pin-project", 2390 + "scoped-tls", 2391 + "serde", 2392 + "serde_json", 2393 + "serde_urlencoded", 2394 + "tokio", 2395 + "tokio-tungstenite", 2396 + "tower-service", 2397 + "urlencoding", 2398 + ] 2399 + 2400 + [[package]] 2401 + name = "wasi" 2402 + version = "0.9.0+wasi-snapshot-preview1" 2403 + source = "registry+https://github.com/rust-lang/crates.io-index" 2404 + checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 2405 + 2406 + [[package]] 2407 + name = "wasm-bindgen" 2408 + version = "0.2.64" 2409 + source = "registry+https://github.com/rust-lang/crates.io-index" 2410 + checksum = "6a634620115e4a229108b71bde263bb4220c483b3f07f5ba514ee8d15064c4c2" 2411 + dependencies = [ 2412 + "cfg-if", 2413 + "serde", 2414 + "serde_json", 2415 + "wasm-bindgen-macro", 2416 + ] 2417 + 2418 + [[package]] 2419 + name = "wasm-bindgen-backend" 2420 + version = "0.2.64" 2421 + source = "registry+https://github.com/rust-lang/crates.io-index" 2422 + checksum = "3e53963b583d18a5aa3aaae4b4c1cb535218246131ba22a71f05b518098571df" 2423 + dependencies = [ 2424 + "bumpalo", 2425 + "lazy_static", 2426 + "log 0.4.8", 2427 + "proc-macro2", 2428 + "quote", 2429 + "syn", 2430 + "wasm-bindgen-shared", 2431 + ] 2432 + 2433 + [[package]] 2434 + name = "wasm-bindgen-futures" 2435 + version = "0.4.14" 2436 + source = "registry+https://github.com/rust-lang/crates.io-index" 2437 + checksum = "dba48d66049d2a6cc8488702e7259ab7afc9043ad0dc5448444f46f2a453b362" 2438 + dependencies = [ 2439 + "cfg-if", 2440 + "js-sys", 2441 + "wasm-bindgen", 2442 + "web-sys", 2443 + ] 2444 + 2445 + [[package]] 2446 + name = "wasm-bindgen-macro" 2447 + version = "0.2.64" 2448 + source = "registry+https://github.com/rust-lang/crates.io-index" 2449 + checksum = "3fcfd5ef6eec85623b4c6e844293d4516470d8f19cd72d0d12246017eb9060b8" 2450 + dependencies = [ 2451 + "quote", 2452 + "wasm-bindgen-macro-support", 2453 + ] 2454 + 2455 + [[package]] 2456 + name = "wasm-bindgen-macro-support" 2457 + version = "0.2.64" 2458 + source = "registry+https://github.com/rust-lang/crates.io-index" 2459 + checksum = "9adff9ee0e94b926ca81b57f57f86d5545cdcb1d259e21ec9bdd95b901754c75" 2460 + dependencies = [ 2461 + "proc-macro2", 2462 + "quote", 2463 + "syn", 2464 + "wasm-bindgen-backend", 2465 + "wasm-bindgen-shared", 2466 + ] 2467 + 2468 + [[package]] 2469 + name = "wasm-bindgen-shared" 2470 + version = "0.2.64" 2471 + source = "registry+https://github.com/rust-lang/crates.io-index" 2472 + checksum = "7f7b90ea6c632dd06fd765d44542e234d5e63d9bb917ecd64d79778a13bd79ae" 2473 + 2474 + [[package]] 2475 + name = "web-sys" 2476 + version = "0.3.41" 2477 + source = "registry+https://github.com/rust-lang/crates.io-index" 2478 + checksum = "863539788676619aac1a23e2df3655e96b32b0e05eb72ca34ba045ad573c625d" 2479 + dependencies = [ 2480 + "js-sys", 2481 + "wasm-bindgen", 2482 + ] 2483 + 2484 + [[package]] 2485 + name = "winapi" 2486 + version = "0.2.8" 2487 + source = "registry+https://github.com/rust-lang/crates.io-index" 2488 + checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 2489 + 2490 + [[package]] 2491 + name = "winapi" 2492 + version = "0.3.9" 2493 + source = "registry+https://github.com/rust-lang/crates.io-index" 2494 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2495 + dependencies = [ 2496 + "winapi-i686-pc-windows-gnu", 2497 + "winapi-x86_64-pc-windows-gnu", 2498 + ] 2499 + 2500 + [[package]] 2501 + name = "winapi-build" 2502 + version = "0.1.1" 2503 + source = "registry+https://github.com/rust-lang/crates.io-index" 2504 + checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 2505 + 2506 + [[package]] 2507 + name = "winapi-i686-pc-windows-gnu" 2508 + version = "0.4.0" 2509 + source = "registry+https://github.com/rust-lang/crates.io-index" 2510 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2511 + 2512 + [[package]] 2513 + name = "winapi-util" 2514 + version = "0.1.5" 2515 + source = "registry+https://github.com/rust-lang/crates.io-index" 2516 + checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 2517 + dependencies = [ 2518 + "winapi 0.3.9", 2519 + ] 2520 + 2521 + [[package]] 2522 + name = "winapi-x86_64-pc-windows-gnu" 2523 + version = "0.4.0" 2524 + source = "registry+https://github.com/rust-lang/crates.io-index" 2525 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2526 + 2527 + [[package]] 2528 + name = "winreg" 2529 + version = "0.7.0" 2530 + source = "registry+https://github.com/rust-lang/crates.io-index" 2531 + checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" 2532 + dependencies = [ 2533 + "winapi 0.3.9", 2534 + ] 2535 + 2536 + [[package]] 2537 + name = "ws2_32-sys" 2538 + version = "0.2.1" 2539 + source = "registry+https://github.com/rust-lang/crates.io-index" 2540 + checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" 2541 + dependencies = [ 2542 + "winapi 0.2.8", 2543 + "winapi-build", 2544 + ] 2545 + 2546 + [[package]] 2547 + name = "xesite" 2548 + version = "2.0.0" 2549 + dependencies = [ 2550 + "anyhow", 2551 + "atom_syndication", 2552 + "chrono", 2553 + "comrak", 2554 + "envy", 2555 + "glob", 2556 + "go_vanity", 2557 + "hyper", 2558 + "jsonfeed", 2559 + "kankyo", 2560 + "lazy_static", 2561 + "log 0.4.8", 2562 + "mime 0.3.16", 2563 + "patreon", 2564 + "pretty_env_logger", 2565 + "prometheus", 2566 + "rand 0.7.3", 2567 + "rss", 2568 + "ructe", 2569 + "serde", 2570 + "serde_dhall", 2571 + "serde_yaml", 2572 + "sitemap", 2573 + "thiserror", 2574 + "tokio", 2575 + "warp", 2576 + "xml-rs", 2577 + ] 2578 + 2579 + [[package]] 2580 + name = "xml-rs" 2581 + version = "0.8.3" 2582 + source = "registry+https://github.com/rust-lang/crates.io-index" 2583 + checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" 2584 + 2585 + [[package]] 2586 + name = "yaml-rust" 2587 + version = "0.4.4" 2588 + source = "registry+https://github.com/rust-lang/crates.io-index" 2589 + checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" 2590 + dependencies = [ 2591 + "linked-hash-map", 2592 + ]
+48
Cargo.toml
··· 1 + [package] 2 + name = "xesite" 3 + version = "2.0.0" 4 + authors = ["Christine Dodrill <me@christine.website>"] 5 + edition = "2018" 6 + build = "src/build.rs" 7 + 8 + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 + 10 + [dependencies] 11 + anyhow = "1" 12 + atom_syndication = { version = "0.9", features = ["with-serde"] } 13 + chrono = "0.4" 14 + comrak = "0.8" 15 + envy = "0.4" 16 + glob = "0.3" 17 + hyper = "0.13" 18 + kankyo = "0.3" 19 + lazy_static = "1.4" 20 + log = "0" 21 + mime = "0.3.0" 22 + pretty_env_logger = "0" 23 + prometheus = { version = "0.9", default-features = false, features = ["process"] } 24 + rand = "0" 25 + rss = "1" 26 + serde_dhall = "0.5.3" 27 + serde = { version = "1", features = ["derive"] } 28 + serde_yaml = "0.8" 29 + sitemap = "0.4" 30 + thiserror = "1" 31 + tokio = { version = "0.2", features = ["macros"] } 32 + warp = "0.2" 33 + xml-rs = "0.8" 34 + 35 + # workspace dependencies 36 + go_vanity = { path = "./lib/go_vanity" } 37 + jsonfeed = { path = "./lib/jsonfeed" } 38 + patreon = { path = "./lib/patreon" } 39 + 40 + [build-dependencies] 41 + ructe = { version = "0.11", features = ["warp02"] } 42 + 43 + [workspace] 44 + members = [ 45 + "./lib/go_vanity", 46 + "./lib/jsonfeed", 47 + "./lib/patreon" 48 + ]
-20
Dockerfile
··· 1 - FROM xena/go:1.14 AS build 2 - ENV GOPROXY https://cache.greedo.xeserv.us 3 - COPY . /site 4 - WORKDIR /site 5 - RUN CGO_ENABLED=0 go test -v ./... 6 - RUN CGO_ENABLED=0 GOBIN=/root go install -v ./cmd/site 7 - 8 - FROM xena/alpine 9 - EXPOSE 5000 10 - WORKDIR /site 11 - COPY --from=build /root/site . 12 - COPY ./static /site/static 13 - COPY ./templates /site/templates 14 - COPY ./blog /site/blog 15 - COPY ./talks /site/talks 16 - COPY ./gallery /site/gallery 17 - COPY ./css /site/css 18 - COPY ./signalboost.dhall /site/signalboost.dhall 19 - HEALTHCHECK CMD wget --spider http://127.0.0.1:5000/.within/health || exit 1 20 - CMD ./site
+1 -1
LICENSE
··· 1 - Copyright (c) 2017 Christine Dodrill <me@christine.website> 1 + Copyright (c) 2017-2020 Christine Dodrill <me@christine.website> 2 2 3 3 This software is provided 'as-is', without any express or implied 4 4 warranty. In no event will the authors be held liable for any damages
+5 -2
README.md
··· 1 1 # site 2 2 3 - My personal/portfolio website. 3 + [![built with 4 + nix](https://builtwithnix.org/badge.svg)](https://builtwithnix.org) 5 + ![Nix](https://github.com/Xe/site/workflows/Nix/badge.svg) 6 + ![Rust](https://github.com/Xe/site/workflows/Rust/badge.svg) 4 7 5 - ![https://puu.sh/vWnJx/57cda175d8.png](https://puu.sh/vWnJx/57cda175d8.png) 8 + My personal/portfolio website.
blog/OVE-20190623-0001.md blog/OVE-20190623-0001.markdown
blog/OVE-20191021-0001.md blog/OVE-20191021-0001.markdown
+189
blog/site-update-2020-07-16.markdown
··· 1 + --- 2 + title: "Site Update: Rewrite in Rust" 3 + date: 2020-07-16 4 + tags: 5 + - rust 6 + --- 7 + 8 + # Site Update: Rewrite in Rust 9 + 10 + Hello there! You are reading this post thanks to a lot of effort, research and 11 + consultation that has resulted in a complete from-scratch rewrite of this 12 + website in [Rust](https://rust-lang.org). The original implementation in Go is 13 + available [here](https://github.com/Xe/site/releases/tag/v1.5.0) should anyone 14 + want to reference that for any reason. 15 + 16 + If you find any issues with the [RSS feed](/blog.rss), [Atom feed](/blog.atom) 17 + or [JSONFeed](/blog.json), please let me know as soon as possible so I can fix 18 + them. 19 + 20 + This website stands on the shoulder of giants. Here are just a few of those and 21 + how they add up into this whole package. 22 + 23 + ## comrak 24 + 25 + All of my posts are written in 26 + [markdown](https://github.com/Xe/site/blob/master/blog/all-there-is-is-now-2019-05-25.markdown). 27 + [comrak](https://github.com/kivikakk/comrak) is a markdown parser written by a 28 + friend of mine that is as fast and as correct as possible. comrak does the job 29 + of turning all of that markdown (over 150 files at the time of writing this 30 + post) into the HTML that you are reading right now. It also supports a lot of 31 + common markdown extensions, which I use heavily in my posts. 32 + 33 + ## warp 34 + 35 + [warp](https://github.com/seanmonstar/warp) is the web framework I use for Rust. 36 + It gives users a set of filters that add up into entire web applications. For an 37 + example, see this example from its readme: 38 + 39 + ```rust 40 + use warp::Filter; 41 + 42 + #[tokio::main] 43 + async fn main() { 44 + // GET /hello/warp => 200 OK with body "Hello, warp!" 45 + let hello = warp::path!("hello" / String) 46 + .map(|name| format!("Hello, {}!", name)); 47 + 48 + warp::serve(hello) 49 + .run(([127, 0, 0, 1], 3030)) 50 + .await; 51 + } 52 + ``` 53 + 54 + This can then be built up into something like this: 55 + 56 + ```rust 57 + let site = index 58 + .or(contact.or(feeds).or(resume.or(signalboost)).or(patrons)) 59 + .or(blog_index.or(series.or(series_view).or(post_view))) 60 + .or(gallery_index.or(gallery_post_view)) 61 + .or(talk_index.or(talk_post_view)) 62 + .or(jsonfeed.or(atom).or(rss.or(sitemap))) 63 + .or(files.or(css).or(favicon).or(sw.or(robots))) 64 + .or(healthcheck.or(metrics_endpoint).or(go_vanity_jsonfeed)) 65 + // ... 66 + ``` 67 + 68 + which is the actual routing setup for this website! 69 + 70 + ## ructe 71 + 72 + In the previous version of this site, I used Go's 73 + [html/template](https://godoc.org/html/template). Rust does not have an 74 + equivalent of html/template in its standard library. After some research, I 75 + settled on [ructe](https://github.com/kaj/ructe) for the HTML templates. ructe 76 + works by preprocessing templates using a little domain-specific language that 77 + compiles down to Rust source code. This makes the templates become optimized 78 + with the rest of the program and enables my website to render most pages in less 79 + than 100 microseconds. Here is an example template (the one for 80 + [/patrons](/patrons)): 81 + 82 + ```html 83 + @use patreon::Users; 84 + @use super::{header_html, footer_html}; 85 + 86 + @(users: Users) 87 + 88 + @:header_html(Some("Patrons"), None) 89 + 90 + <h1>Patrons</h1> 91 + 92 + <p>These awesome people donate to me on <a href="https://patreon.com/cadey">Patreon</a>. 93 + If you would like to show up in this list, please donate to me on Patreon. This 94 + is refreshed every time the site is deployed.</p> 95 + 96 + <p> 97 + <ul> 98 + @for user in users { 99 + <li>@user.attributes.full_name</li> 100 + } 101 + </ul> 102 + </p> 103 + 104 + @:footer_html() 105 + ``` 106 + 107 + The templates compile down to Rust, which lets me include other parts of the 108 + program into the templates. Here I use that to take a list of users from the 109 + incredibly hacky Patreon API client I wrote for this website and iterate over 110 + it, making a list of every patron by name. 111 + 112 + ## Build Process 113 + 114 + As a nice side effect of this rewrite, my website is now completely built using 115 + [Nix](https://nixos.org/). This allows the website to be built reproducibly, as 116 + well as a full development environment setup for free for anyone that checks out 117 + the repo and runs `nix-shell`. Check out 118 + [naersk](https://github.com/nmattia/naersk) for the secret sauce that enables my 119 + docker image build. See [this blogpost](/blog/drone-kubernetes-cd-2020-07-10) 120 + for more information about this build process (though my site uses GitHub 121 + Actions instead of Drone). 122 + 123 + ## `jsonfeed` Go package 124 + 125 + I used to have a [JSONFeed](https://www.jsonfeed.org/) package publicly visible 126 + at the go import path `christine.website/jsonfeed`. As far as I know I'm the 127 + only person who ended up using it; but in case there are any private repos that 128 + I don't know about depending on it, I have made the jsonfeed package available 129 + at its old location as well as its source code 130 + [here](https://tulpa.dev/Xe/jsonfeed). You may have to update your `go.mod` file 131 + to import `christine.website/jsonfeed` instead of `christine.website`. If 132 + something ends up going wrong as a result of this, please [file a GitHub issue 133 + here](https://github.com/Xe/site/issues/new) and I can attempt to assist 134 + further. 135 + 136 + ## `go_vanity` crate 137 + 138 + I have written a small go vanity import crate and exposed it in my Git repo. If 139 + you want to use it, add it to your `Cargo.toml` like this: 140 + 141 + ```toml 142 + [dependencies] 143 + go_vanity = { git = "https://github.com/Xe/site", branch = "master" } 144 + ``` 145 + 146 + You can then use it from any warp application by calling `go_vanity::github` or 147 + `go_vanity::gitea` like this: 148 + 149 + ```rust 150 + let go_vanity_jsonfeed = warp::path("jsonfeed") 151 + .and(warp::any().map(move || "christine.website/jsonfeed")) 152 + .and(warp::any().map(move || "https://tulpa.dev/Xe/jsonfeed")) 153 + .and_then(go_vanity::gitea); 154 + ``` 155 + 156 + I plan to add full documentation to this crate soon as well as release it 157 + properly on crates.io. 158 + 159 + ## `patreon` crate 160 + 161 + I have also written a small [Patreon](https://www.patreon.com/) API client and 162 + made it available in my Git repo. If you want to use it, add it to your 163 + `Cargo.toml` like this: 164 + 165 + ```toml 166 + [dependencies] 167 + patreon = { git = "https://github.com/Xe/site", branch = "master" } 168 + ``` 169 + 170 + This client is _incredibly limited_ and only supports the minimum parts of the 171 + Patreon API that are required for my website to function. Patreon has also 172 + apparently started to phase out support for its API anyways, so I don't know how 173 + long this will be useful. 174 + 175 + But this is there should you need it! 176 + 177 + ## Dhall Kubernetes Manifest 178 + 179 + I also took the time to port the kubernetes manifest to 180 + [Dhall](https://dhall-lang.org/). This allows me to have a type-safe kubernetes 181 + manifest that will correctly have all of the secrets injected for me from the 182 + environment of the deploy script. 183 + 184 + --- 185 + 186 + These are the biggest giants that my website now sits on. The code for this 187 + rewrite is still a bit messy. I'm working on making it better, but my goal is to 188 + have this website's code shine as an example of how to best write this kind of 189 + website in Rust. Check out the code [here](https://github.com/Xe/site).
-25
cmd/site/clacks.go
··· 1 - package main 2 - 3 - import ( 4 - "math/rand" 5 - "net/http" 6 - "time" 7 - ) 8 - 9 - type ClackSet []string 10 - 11 - func (cs ClackSet) Name() string { 12 - return "GNU " + cs[rand.Intn(len(cs))] 13 - } 14 - 15 - func (cs ClackSet) Middleware(next http.Handler) http.Handler { 16 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 - w.Header().Add("X-Clacks-Overhead", cs.Name()) 18 - 19 - next.ServeHTTP(w, r) 20 - }) 21 - } 22 - 23 - func init() { 24 - rand.Seed(time.Now().Unix()) 25 - }
-245
cmd/site/html.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "html/template" 7 - "net/http" 8 - "path/filepath" 9 - "strings" 10 - "time" 11 - 12 - "christine.website/cmd/site/internal" 13 - "christine.website/cmd/site/internal/blog" 14 - "github.com/prometheus/client_golang/prometheus" 15 - "github.com/prometheus/client_golang/prometheus/promauto" 16 - "within.website/ln" 17 - "within.website/ln/opname" 18 - ) 19 - 20 - var ( 21 - templateRenderTime = promauto.NewHistogramVec(prometheus.HistogramOpts{ 22 - Name: "template_render_time", 23 - Help: "Template render time in nanoseconds", 24 - }, []string{"name"}) 25 - ) 26 - 27 - func logTemplateTime(ctx context.Context, name string, f ln.F, from time.Time) { 28 - dur := time.Since(from) 29 - templateRenderTime.With(prometheus.Labels{"name": name}).Observe(float64(dur)) 30 - ln.Log(ctx, f, ln.F{"dur": dur, "name": name}) 31 - } 32 - 33 - func (s *Site) renderTemplatePage(templateFname string, data interface{}) http.Handler { 34 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 - ctx := opname.With(r.Context(), "renderTemplatePage") 36 - fetag := "W/" + internal.Hash(templateFname, etag) + "-1" 37 - 38 - f := ln.F{"etag": fetag, "if_none_match": r.Header.Get("If-None-Match")} 39 - 40 - if r.Header.Get("If-None-Match") == fetag { 41 - http.Error(w, "Cached data OK", http.StatusNotModified) 42 - ln.Log(ctx, f, ln.Info("Cache hit")) 43 - return 44 - } 45 - 46 - defer logTemplateTime(ctx, templateFname, f, time.Now()) 47 - 48 - var t *template.Template 49 - var err error 50 - 51 - t, err = template.ParseFiles("templates/base.html", "templates/"+templateFname) 52 - if err != nil { 53 - w.WriteHeader(http.StatusInternalServerError) 54 - ln.Error(ctx, err, ln.F{"action": "renderTemplatePage", "page": templateFname}) 55 - fmt.Fprintf(w, "error: %v", err) 56 - } 57 - 58 - w.Header().Set("ETag", fetag) 59 - w.Header().Set("Cache-Control", "max-age=432000") 60 - 61 - err = t.Execute(w, data) 62 - if err != nil { 63 - panic(err) 64 - } 65 - }) 66 - } 67 - 68 - var postView = promauto.NewCounterVec(prometheus.CounterOpts{ 69 - Name: "posts_viewed", 70 - Help: "The number of views per post or talk", 71 - }, []string{"base"}) 72 - 73 - func (s *Site) listSeries(w http.ResponseWriter, r *http.Request) { 74 - s.renderTemplatePage("series.html", s.Series).ServeHTTP(w, r) 75 - } 76 - 77 - func (s *Site) showSeries(w http.ResponseWriter, r *http.Request) { 78 - if r.RequestURI == "/blog/series/" { 79 - http.Redirect(w, r, "/blog/series", http.StatusSeeOther) 80 - return 81 - } 82 - 83 - series := filepath.Base(r.URL.Path) 84 - var posts []blog.Post 85 - 86 - for _, p := range s.Posts { 87 - if p.Series == series { 88 - posts = append(posts, p) 89 - } 90 - } 91 - 92 - s.renderTemplatePage("serieslist.html", struct { 93 - Name string 94 - Posts []blog.Post 95 - }{ 96 - Name: series, 97 - Posts: posts, 98 - }).ServeHTTP(w, r) 99 - } 100 - 101 - func (s *Site) showGallery(w http.ResponseWriter, r *http.Request) { 102 - if r.RequestURI == "/gallery/" { 103 - http.Redirect(w, r, "/gallery", http.StatusSeeOther) 104 - return 105 - } 106 - 107 - cmp := r.URL.Path[1:] 108 - var p blog.Post 109 - var found bool 110 - for _, pst := range s.Gallery { 111 - if pst.Link == cmp { 112 - p = pst 113 - found = true 114 - } 115 - } 116 - 117 - if !found { 118 - w.WriteHeader(http.StatusNotFound) 119 - s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r) 120 - return 121 - } 122 - 123 - var tags string 124 - if len(p.Tags) != 0 { 125 - for _, t := range p.Tags { 126 - tags = tags + " #" + strings.ReplaceAll(t, "-", "") 127 - } 128 - } 129 - 130 - h := s.renderTemplatePage("gallerypost.html", struct { 131 - Title string 132 - Link string 133 - BodyHTML template.HTML 134 - Date string 135 - Tags string 136 - Image string 137 - }{ 138 - Title: p.Title, 139 - Link: p.Link, 140 - BodyHTML: p.BodyHTML, 141 - Date: internal.IOS13Detri(p.Date), 142 - Tags: tags, 143 - Image: p.ImageURL, 144 - }) 145 - 146 - if h == nil { 147 - panic("how did we get here?") 148 - } 149 - 150 - h.ServeHTTP(w, r) 151 - postView.With(prometheus.Labels{"base": filepath.Base(p.Link)}).Inc() 152 - } 153 - 154 - func (s *Site) showTalk(w http.ResponseWriter, r *http.Request) { 155 - if r.RequestURI == "/talks/" { 156 - http.Redirect(w, r, "/talks", http.StatusSeeOther) 157 - return 158 - } 159 - 160 - cmp := r.URL.Path[1:] 161 - var p blog.Post 162 - var found bool 163 - for _, pst := range s.Talks { 164 - if pst.Link == cmp { 165 - p = pst 166 - found = true 167 - } 168 - } 169 - 170 - if !found { 171 - w.WriteHeader(http.StatusNotFound) 172 - s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r) 173 - return 174 - } 175 - 176 - h := s.renderTemplatePage("talkpost.html", struct { 177 - Title string 178 - Link string 179 - BodyHTML template.HTML 180 - Date string 181 - SlidesLink string 182 - }{ 183 - Title: p.Title, 184 - Link: p.Link, 185 - BodyHTML: p.BodyHTML, 186 - Date: internal.IOS13Detri(p.Date), 187 - SlidesLink: p.SlidesLink, 188 - }) 189 - 190 - if h == nil { 191 - panic("how did we get here?") 192 - } 193 - 194 - h.ServeHTTP(w, r) 195 - postView.With(prometheus.Labels{"base": filepath.Base(p.Link)}).Inc() 196 - } 197 - 198 - func (s *Site) showPost(w http.ResponseWriter, r *http.Request) { 199 - if r.RequestURI == "/blog/" { 200 - http.Redirect(w, r, "/blog", http.StatusSeeOther) 201 - return 202 - } 203 - 204 - cmp := r.URL.Path[1:] 205 - var p blog.Post 206 - var found bool 207 - for _, pst := range s.Posts { 208 - if pst.Link == cmp { 209 - p = pst 210 - found = true 211 - } 212 - } 213 - 214 - if !found { 215 - w.WriteHeader(http.StatusNotFound) 216 - s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r) 217 - return 218 - } 219 - 220 - var tags string 221 - 222 - if len(p.Tags) != 0 { 223 - for _, t := range p.Tags { 224 - tags = tags + " #" + strings.ReplaceAll(t, "-", "") 225 - } 226 - } 227 - 228 - s.renderTemplatePage("blogpost.html", struct { 229 - Title string 230 - Link string 231 - BodyHTML template.HTML 232 - Date string 233 - Series, SeriesTag string 234 - Tags string 235 - }{ 236 - Title: p.Title, 237 - Link: p.Link, 238 - BodyHTML: p.BodyHTML, 239 - Date: internal.IOS13Detri(p.Date), 240 - Series: p.Series, 241 - SeriesTag: strings.ReplaceAll(p.Series, "-", ""), 242 - Tags: tags, 243 - }).ServeHTTP(w, r) 244 - postView.With(prometheus.Labels{"base": filepath.Base(p.Link)}).Inc() 245 - }
-137
cmd/site/internal/blog/blog.go
··· 1 - package blog 2 - 3 - import ( 4 - "html/template" 5 - "io/ioutil" 6 - "os" 7 - "path/filepath" 8 - "sort" 9 - "strings" 10 - "time" 11 - 12 - "christine.website/cmd/site/internal/front" 13 - "github.com/russross/blackfriday" 14 - ) 15 - 16 - // Post is a single blogpost. 17 - type Post struct { 18 - Title string `json:"title"` 19 - Link string `json:"link"` 20 - Summary string `json:"summary,omitifempty"` 21 - Body string `json:"-"` 22 - BodyHTML template.HTML `json:"body"` 23 - Series string `json:"series"` 24 - Tags []string `json:"tags"` 25 - SlidesLink string `json:"slides_link"` 26 - ImageURL string `json:"image_url"` 27 - ThumbURL string `json:"thumb_url"` 28 - Date time.Time 29 - DateString string `json:"date"` 30 - } 31 - 32 - // Posts implements sort.Interface for a slice of Post objects. 33 - type Posts []Post 34 - 35 - func (p Posts) Series() []string { 36 - names := map[string]struct{}{} 37 - 38 - for _, ps := range p { 39 - if ps.Series != "" { 40 - names[ps.Series] = struct{}{} 41 - } 42 - } 43 - 44 - var result []string 45 - 46 - for name := range names { 47 - result = append(result, name) 48 - } 49 - 50 - return result 51 - } 52 - 53 - func (p Posts) Len() int { return len(p) } 54 - func (p Posts) Less(i, j int) bool { 55 - iDate := p[i].Date 56 - jDate := p[j].Date 57 - 58 - return iDate.Unix() < jDate.Unix() 59 - } 60 - func (p Posts) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 61 - 62 - // LoadPosts loads posts for a given directory. 63 - func LoadPosts(path string, prepend string) (Posts, error) { 64 - type postFM struct { 65 - Title string 66 - Date string 67 - Series string 68 - Tags []string 69 - SlidesLink string `yaml:"slides_link"` 70 - Image string 71 - Thumb string 72 - Show string 73 - } 74 - var result Posts 75 - 76 - err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 77 - if err != nil { 78 - return err 79 - } 80 - 81 - if info.IsDir() { 82 - return nil 83 - } 84 - 85 - fin, err := os.Open(path) 86 - if err != nil { 87 - return err 88 - } 89 - defer fin.Close() 90 - 91 - content, err := ioutil.ReadAll(fin) 92 - if err != nil { 93 - return err 94 - } 95 - 96 - var fm postFM 97 - remaining, err := front.Unmarshal(content, &fm) 98 - if err != nil { 99 - return err 100 - } 101 - 102 - output := blackfriday.Run(remaining) 103 - 104 - const timeFormat = `2006-01-02` 105 - date, err := time.Parse(timeFormat, fm.Date) 106 - if err != nil { 107 - return err 108 - } 109 - 110 - fname := filepath.Base(path) 111 - fname = strings.TrimSuffix(fname, filepath.Ext(fname)) 112 - 113 - p := Post{ 114 - Title: fm.Title, 115 - Date: date, 116 - DateString: fm.Date, 117 - Link: filepath.Join(prepend, fname), 118 - Body: string(remaining), 119 - BodyHTML: template.HTML(output), 120 - SlidesLink: fm.SlidesLink, 121 - Series: fm.Series, 122 - Tags: fm.Tags, 123 - ImageURL: fm.Image, 124 - ThumbURL: fm.Thumb, 125 - } 126 - result = append(result, p) 127 - 128 - return nil 129 - }) 130 - if err != nil { 131 - return nil, err 132 - } 133 - 134 - sort.Sort(sort.Reverse(result)) 135 - 136 - return result, nil 137 - }
-66
cmd/site/internal/blog/blog_test.go
··· 1 - package blog 2 - 3 - import ( 4 - "testing" 5 - ) 6 - 7 - func TestLoadPosts(t *testing.T) { 8 - posts, err := LoadPosts("../../../../blog", "blog") 9 - if err != nil { 10 - t.Fatal(err) 11 - } 12 - 13 - for _, post := range posts { 14 - t.Run(post.Link, post.test) 15 - } 16 - } 17 - 18 - func TestLoadTalks(t *testing.T) { 19 - talks, err := LoadPosts("../../../../talks", "talks") 20 - if err != nil { 21 - t.Fatal(err) 22 - } 23 - 24 - for _, talk := range talks { 25 - t.Run(talk.Link, talk.test) 26 - if talk.SlidesLink == "" { 27 - t.Errorf("talk %s (%s) doesn't have a slides link", talk.Title, talk.DateString) 28 - } 29 - } 30 - } 31 - 32 - func TestLoadGallery(t *testing.T) { 33 - gallery, err := LoadPosts("../../../../gallery", "gallery") 34 - if err != nil { 35 - t.Fatal(err) 36 - } 37 - 38 - for _, art := range gallery { 39 - t.Run(art.Link, art.test) 40 - if art.ImageURL == "" { 41 - t.Errorf("art %s (%s) doesn't have an image link", art.Title, art.DateString) 42 - } 43 - if art.ThumbURL == "" { 44 - t.Errorf("art %s (%s) doesn't have a thumbnail link", art.Title, art.DateString) 45 - } 46 - 47 - } 48 - } 49 - 50 - func (p Post) test(t *testing.T) { 51 - if p.Title == "" { 52 - t.Error("no post title") 53 - } 54 - 55 - if p.DateString == "" { 56 - t.Error("no date") 57 - } 58 - 59 - if p.Link == "" { 60 - t.Error("no link") 61 - } 62 - 63 - if p.Body == "" { 64 - t.Error("no body") 65 - } 66 - }
-10
cmd/site/internal/date.go
··· 1 - package internal 2 - 3 - import "time" 4 - 5 - const iOS13DetriFormat = `2006 M1 2` 6 - 7 - // IOS13Detri formats a datestamp like iOS 13 does with the Lojban locale. 8 - func IOS13Detri(t time.Time) string { 9 - return t.Format(iOS13DetriFormat) 10 - }
-28
cmd/site/internal/date_test.go
··· 1 - package internal 2 - 3 - import ( 4 - "fmt" 5 - "testing" 6 - "time" 7 - ) 8 - 9 - func TestIOS13Detri(t *testing.T) { 10 - cases := []struct { 11 - in time.Time 12 - out string 13 - }{ 14 - { 15 - in: time.Date(2019, time.March, 30, 0, 0, 0, 0, time.FixedZone("UTC", 0)), 16 - out: "2019 M3 30", 17 - }, 18 - } 19 - 20 - for _, cs := range cases { 21 - t.Run(fmt.Sprintf("%s -> %s", cs.in.Format(time.RFC3339), cs.out), func(t *testing.T) { 22 - result := IOS13Detri(cs.in) 23 - if result != cs.out { 24 - t.Fatalf("wanted: %s, got: %s", cs.out, result) 25 - } 26 - }) 27 - } 28 - }
-19
cmd/site/internal/front/LICENSE
··· 1 - Copyright (c) 2017 TJ Holowaychuk <tj@vision-media.ca> 2 - 3 - Permission is hereby granted, free of charge, to any person obtaining a copy 4 - of this software and associated documentation files (the "Software"), to deal 5 - in the Software without restriction, including without limitation the rights 6 - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 - copies of the Software, and to permit persons to whom the Software is 8 - furnished to do so, subject to the following conditions: 9 - 10 - The above copyright notice and this permission notice shall be included in 11 - all copies or substantial portions of the Software. 12 - 13 - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 - THE SOFTWARE.
-24
cmd/site/internal/front/front.go
··· 1 - // Package front provides YAML frontmatter unmarshalling. 2 - package front 3 - 4 - import ( 5 - "bytes" 6 - 7 - "gopkg.in/yaml.v2" 8 - ) 9 - 10 - // Delimiter. 11 - var delim = []byte("---") 12 - 13 - // Unmarshal parses YAML frontmatter and returns the content. When no 14 - // frontmatter delimiters are present the original content is returned. 15 - func Unmarshal(b []byte, v interface{}) (content []byte, err error) { 16 - if !bytes.HasPrefix(b, delim) { 17 - return b, nil 18 - } 19 - 20 - parts := bytes.SplitN(b, delim, 3) 21 - content = parts[2] 22 - err = yaml.Unmarshal(parts[1], v) 23 - return 24 - }
-42
cmd/site/internal/front/front_test.go
··· 1 - package front_test 2 - 3 - import ( 4 - "fmt" 5 - "log" 6 - 7 - "christine.website/cmd/site/internal/front" 8 - ) 9 - 10 - var markdown = []byte(`--- 11 - title: Ferrets 12 - authors: 13 - - Tobi 14 - - Loki 15 - - Jane 16 - --- 17 - Some content here, so 18 - interesting, you just 19 - want to keep reading.`) 20 - 21 - type article struct { 22 - Title string 23 - Authors []string 24 - } 25 - 26 - func Example() { 27 - var a article 28 - 29 - content, err := front.Unmarshal(markdown, &a) 30 - if err != nil { 31 - log.Fatalf("error unmarshalling: %s", err) 32 - } 33 - 34 - fmt.Printf("%#v\n", a) 35 - fmt.Printf("%s\n", string(content)) 36 - // Output: 37 - // front_test.article{Title:"Ferrets", Authors:[]string{"Tobi", "Loki", "Jane"}} 38 - // 39 - // Some content here, so 40 - // interesting, you just 41 - // want to keep reading. 42 - }
-14
cmd/site/internal/hash.go
··· 1 - package internal 2 - 3 - import ( 4 - "crypto/md5" 5 - "fmt" 6 - ) 7 - 8 - // Hash is a simple wrapper around the MD5 algorithm implementation in the 9 - // Go standard library. It takes in data and a salt and returns the hashed 10 - // representation. 11 - func Hash(data string, salt string) string { 12 - output := md5.Sum([]byte(data + salt)) 13 - return fmt.Sprintf("%x", output) 14 - }
-43
cmd/site/internal/middleware/metrics.go
··· 1 - package middleware 2 - 3 - import ( 4 - "net/http" 5 - 6 - "github.com/prometheus/client_golang/prometheus" 7 - "github.com/prometheus/client_golang/prometheus/promhttp" 8 - ) 9 - 10 - var ( 11 - requestCounter = prometheus.NewCounterVec( 12 - prometheus.CounterOpts{ 13 - Name: "handler_requests_total", 14 - Help: "Total number of request/responses by HTTP status code.", 15 - }, []string{"handler", "code"}) 16 - 17 - requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 18 - Name: "handler_request_duration", 19 - Help: "Handler request duration.", 20 - }, []string{"handler", "method"}) 21 - 22 - requestInFlight = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 23 - Name: "handler_requests_in_flight", 24 - Help: "Current number of requests being served.", 25 - }, []string{"handler"}) 26 - ) 27 - 28 - func init() { 29 - _ = prometheus.Register(requestCounter) 30 - _ = prometheus.Register(requestDuration) 31 - _ = prometheus.Register(requestInFlight) 32 - } 33 - 34 - // Metrics captures request duration, request count and in-flight request count 35 - // metrics for HTTP handlers. The family field is used to discriminate handlers. 36 - func Metrics(family string, next http.Handler) http.Handler { 37 - return promhttp.InstrumentHandlerDuration( 38 - requestDuration.MustCurryWith(prometheus.Labels{"handler": family}), 39 - promhttp.InstrumentHandlerCounter(requestCounter.MustCurryWith(prometheus.Labels{"handler": family}), 40 - promhttp.InstrumentHandlerInFlight(requestInFlight.With(prometheus.Labels{"handler": family}), next), 41 - ), 42 - ) 43 - }
-31
cmd/site/internal/middleware/requestid.go
··· 1 - package middleware 2 - 3 - import ( 4 - "net/http" 5 - 6 - "github.com/celrenheit/sandflake" 7 - "within.website/ln" 8 - ) 9 - 10 - // RequestID appends a unique (sandflake) request ID to each request's 11 - // X-Request-Id header field, much like Heroku's router does. 12 - func RequestID(next http.Handler) http.Handler { 13 - var g sandflake.Generator 14 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 - id := g.Next().String() 16 - 17 - if rid := r.Header.Get("X-Request-Id"); rid != "" { 18 - id = rid + "," + id 19 - } 20 - 21 - ctx := ln.WithF(r.Context(), ln.F{ 22 - "request_id": id, 23 - }) 24 - r = r.WithContext(ctx) 25 - 26 - w.Header().Set("X-Request-Id", id) 27 - r.Header.Set("X-Request-Id", id) 28 - 29 - next.ServeHTTP(w, r) 30 - }) 31 - }
-296
cmd/site/main.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "html/template" 6 - "io/ioutil" 7 - "net/http" 8 - "os" 9 - "sort" 10 - "strings" 11 - "time" 12 - 13 - "christine.website/cmd/site/internal/blog" 14 - "christine.website/cmd/site/internal/middleware" 15 - "christine.website/jsonfeed" 16 - "github.com/gorilla/feeds" 17 - _ "github.com/joho/godotenv/autoload" 18 - "github.com/povilasv/prommod" 19 - "github.com/prometheus/client_golang/prometheus" 20 - "github.com/prometheus/client_golang/prometheus/promhttp" 21 - blackfriday "github.com/russross/blackfriday" 22 - "github.com/sebest/xff" 23 - "github.com/snabb/sitemap" 24 - "within.website/ln" 25 - "within.website/ln/ex" 26 - "within.website/ln/opname" 27 - ) 28 - 29 - var port = os.Getenv("PORT") 30 - 31 - func main() { 32 - if port == "" { 33 - port = "29384" 34 - } 35 - 36 - ctx := ln.WithF(opname.With(context.Background(), "main"), ln.F{ 37 - "port": port, 38 - "git_rev": gitRev, 39 - }) 40 - 41 - _ = prometheus.Register(prommod.NewCollector("christine")) 42 - 43 - s, err := Build() 44 - if err != nil { 45 - ln.FatalErr(ctx, err, ln.Action("Build")) 46 - } 47 - 48 - mux := http.NewServeMux() 49 - mux.HandleFunc("/.within/health", func(w http.ResponseWriter, r *http.Request) { 50 - http.Error(w, "OK", http.StatusOK) 51 - }) 52 - mux.Handle("/", s) 53 - 54 - ln.Log(ctx, ln.Action("http_listening")) 55 - ln.FatalErr(ctx, http.ListenAndServe(":"+port, mux)) 56 - } 57 - 58 - // Site is the parent object for https://christine.website's backend. 59 - type Site struct { 60 - Posts blog.Posts 61 - Talks blog.Posts 62 - Gallery blog.Posts 63 - Resume template.HTML 64 - Series []string 65 - SignalBoost []Person 66 - 67 - clacks ClackSet 68 - patrons []string 69 - rssFeed *feeds.Feed 70 - jsonFeed *jsonfeed.Feed 71 - 72 - mux *http.ServeMux 73 - xffmw *xff.XFF 74 - } 75 - 76 - var gitRev = os.Getenv("GIT_REV") 77 - 78 - func envOr(key, or string) string { 79 - if result, ok := os.LookupEnv(key); ok { 80 - return result 81 - } 82 - 83 - return or 84 - } 85 - 86 - func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) { 87 - ctx := opname.With(r.Context(), "site.ServeHTTP") 88 - ctx = ln.WithF(ctx, ln.F{ 89 - "user_agent": r.Header.Get("User-Agent"), 90 - }) 91 - r = r.WithContext(ctx) 92 - if gitRev != "" { 93 - w.Header().Add("X-Git-Rev", gitRev) 94 - } 95 - 96 - w.Header().Add("X-Hacker", "If you are reading this, check out /signalboost to find people for your team") 97 - 98 - s.clacks.Middleware( 99 - middleware.RequestID( 100 - s.xffmw.Handler( 101 - ex.HTTPLog(s.mux), 102 - ), 103 - ), 104 - ).ServeHTTP(w, r) 105 - } 106 - 107 - var arbDate = time.Date(2020, time.May, 21, 0, 0, 0, 0, time.UTC) 108 - 109 - // Build creates a new Site instance or fails. 110 - func Build() (*Site, error) { 111 - pc, err := NewPatreonClient() 112 - if err != nil { 113 - return nil, err 114 - } 115 - 116 - pledges, err := GetPledges(pc) 117 - if err != nil { 118 - return nil, err 119 - } 120 - 121 - people, err := loadPeople("./signalboost.dhall") 122 - if err != nil { 123 - return nil, err 124 - } 125 - 126 - smi := sitemap.New() 127 - smi.Add(&sitemap.URL{ 128 - Loc: "https://christine.website/resume", 129 - LastMod: &arbDate, 130 - ChangeFreq: sitemap.Monthly, 131 - }) 132 - 133 - smi.Add(&sitemap.URL{ 134 - Loc: "https://christine.website/contact", 135 - LastMod: &arbDate, 136 - ChangeFreq: sitemap.Monthly, 137 - }) 138 - 139 - smi.Add(&sitemap.URL{ 140 - Loc: "https://christine.website/", 141 - LastMod: &arbDate, 142 - ChangeFreq: sitemap.Monthly, 143 - }) 144 - 145 - smi.Add(&sitemap.URL{ 146 - Loc: "https://christine.website/patrons", 147 - LastMod: &arbDate, 148 - ChangeFreq: sitemap.Weekly, 149 - }) 150 - 151 - smi.Add(&sitemap.URL{ 152 - Loc: "https://christine.website/blog", 153 - LastMod: &arbDate, 154 - ChangeFreq: sitemap.Weekly, 155 - }) 156 - 157 - xffmw, err := xff.Default() 158 - if err != nil { 159 - return nil, err 160 - } 161 - 162 - s := &Site{ 163 - rssFeed: &feeds.Feed{ 164 - Title: "Christine Dodrill's Blog", 165 - Link: &feeds.Link{Href: "https://christine.website/blog"}, 166 - Description: "My blog posts and rants about various technology things.", 167 - Author: &feeds.Author{Name: "Christine Dodrill", Email: "me@christine.website"}, 168 - Created: bootTime, 169 - Copyright: "This work is copyright Christine Dodrill. My viewpoints are my own and not the view of any employer past, current or future.", 170 - }, 171 - jsonFeed: &jsonfeed.Feed{ 172 - Version: jsonfeed.CurrentVersion, 173 - Title: "Christine Dodrill's Blog", 174 - HomePageURL: "https://christine.website", 175 - FeedURL: "https://christine.website/blog.json", 176 - Description: "My blog posts and rants about various technology things.", 177 - UserComment: "This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1", 178 - Icon: icon, 179 - Favicon: icon, 180 - Author: jsonfeed.Author{ 181 - Name: "Christine Dodrill", 182 - Avatar: icon, 183 - }, 184 - }, 185 - mux: http.NewServeMux(), 186 - xffmw: xffmw, 187 - 188 - clacks: ClackSet(strings.Split(envOr("CLACK_SET", "Ashlynn"), ",")), 189 - patrons: pledges, 190 - SignalBoost: people, 191 - } 192 - 193 - posts, err := blog.LoadPosts("./blog/", "blog") 194 - if err != nil { 195 - return nil, err 196 - } 197 - s.Posts = posts 198 - s.Series = posts.Series() 199 - sort.Strings(s.Series) 200 - 201 - talks, err := blog.LoadPosts("./talks", "talks") 202 - if err != nil { 203 - return nil, err 204 - } 205 - s.Talks = talks 206 - 207 - gallery, err := blog.LoadPosts("./gallery", "gallery") 208 - if err != nil { 209 - return nil, err 210 - } 211 - s.Gallery = gallery 212 - 213 - var everything blog.Posts 214 - everything = append(everything, posts...) 215 - everything = append(everything, talks...) 216 - everything = append(everything, gallery...) 217 - 218 - sort.Sort(sort.Reverse(everything)) 219 - 220 - resumeData, err := ioutil.ReadFile("./static/resume/resume.md") 221 - if err != nil { 222 - return nil, err 223 - } 224 - 225 - s.Resume = template.HTML(blackfriday.Run(resumeData)) 226 - 227 - for _, item := range everything { 228 - s.rssFeed.Items = append(s.rssFeed.Items, &feeds.Item{ 229 - Title: item.Title, 230 - Link: &feeds.Link{Href: "https://christine.website/" + item.Link}, 231 - Description: item.Summary, 232 - Created: item.Date, 233 - Content: string(item.BodyHTML), 234 - }) 235 - 236 - s.jsonFeed.Items = append(s.jsonFeed.Items, jsonfeed.Item{ 237 - ID: "https://christine.website/" + item.Link, 238 - URL: "https://christine.website/" + item.Link, 239 - Title: item.Title, 240 - DatePublished: item.Date, 241 - ContentHTML: string(item.BodyHTML), 242 - Tags: item.Tags, 243 - }) 244 - 245 - smi.Add(&sitemap.URL{ 246 - Loc: "https://christine.website/" + item.Link, 247 - LastMod: &item.Date, 248 - ChangeFreq: sitemap.Monthly, 249 - }) 250 - } 251 - 252 - // Add HTTP routes here 253 - s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 254 - if r.URL.Path != "/" { 255 - w.WriteHeader(http.StatusNotFound) 256 - s.renderTemplatePage("error.html", "can't find "+r.URL.Path).ServeHTTP(w, r) 257 - return 258 - } 259 - 260 - s.renderTemplatePage("index.html", nil).ServeHTTP(w, r) 261 - }) 262 - s.mux.Handle("/metrics", promhttp.Handler()) 263 - s.mux.Handle("/feeds", middleware.Metrics("feeds", s.renderTemplatePage("feeds.html", nil))) 264 - s.mux.Handle("/patrons", middleware.Metrics("patrons", s.renderTemplatePage("patrons.html", s.patrons))) 265 - s.mux.Handle("/signalboost", middleware.Metrics("signalboost", s.renderTemplatePage("signalboost.html", s.SignalBoost))) 266 - s.mux.Handle("/resume", middleware.Metrics("resume", s.renderTemplatePage("resume.html", s.Resume))) 267 - s.mux.Handle("/blog", middleware.Metrics("blog", s.renderTemplatePage("blogindex.html", s.Posts))) 268 - s.mux.Handle("/talks", middleware.Metrics("talks", s.renderTemplatePage("talkindex.html", s.Talks))) 269 - s.mux.Handle("/gallery", middleware.Metrics("gallery", s.renderTemplatePage("galleryindex.html", s.Gallery))) 270 - s.mux.Handle("/contact", middleware.Metrics("contact", s.renderTemplatePage("contact.html", nil))) 271 - s.mux.Handle("/blog.rss", middleware.Metrics("blog.rss", http.HandlerFunc(s.createFeed))) 272 - s.mux.Handle("/blog.atom", middleware.Metrics("blog.atom", http.HandlerFunc(s.createAtom))) 273 - s.mux.Handle("/blog.json", middleware.Metrics("blog.json", http.HandlerFunc(s.createJSONFeed))) 274 - s.mux.Handle("/blog/", middleware.Metrics("blogpost", http.HandlerFunc(s.showPost))) 275 - s.mux.Handle("/blog/series", http.HandlerFunc(s.listSeries)) 276 - s.mux.Handle("/blog/series/", http.HandlerFunc(s.showSeries)) 277 - s.mux.Handle("/talks/", middleware.Metrics("talks", http.HandlerFunc(s.showTalk))) 278 - s.mux.Handle("/gallery/", middleware.Metrics("gallery", http.HandlerFunc(s.showGallery))) 279 - s.mux.Handle("/css/", http.FileServer(http.Dir("."))) 280 - s.mux.Handle("/static/", http.FileServer(http.Dir("."))) 281 - s.mux.HandleFunc("/sw.js", func(w http.ResponseWriter, r *http.Request) { 282 - http.ServeFile(w, r, "./static/js/sw.js") 283 - }) 284 - s.mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { 285 - http.ServeFile(w, r, "./static/robots.txt") 286 - }) 287 - s.mux.Handle("/sitemap.xml", middleware.Metrics("sitemap", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 288 - w.Header().Set("Content-Type", "application/xml") 289 - _, _ = smi.WriteTo(w) 290 - }))) 291 - s.mux.HandleFunc("/api/pageview-timer", handlePageViewTimer) 292 - 293 - return s, nil 294 - } 295 - 296 - const icon = "https://christine.website/static/img/avatar.png"
-53
cmd/site/pageview.go
··· 1 - package main 2 - 3 - import ( 4 - "encoding/json" 5 - "io/ioutil" 6 - "net/http" 7 - "time" 8 - 9 - "github.com/prometheus/client_golang/prometheus" 10 - "within.website/ln" 11 - ) 12 - 13 - var ( 14 - readTimes = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 15 - Name: "blogpage_read_times", 16 - Help: "This tracks how much time people spend reading articles on my blog", 17 - }, []string{"path"}) 18 - ) 19 - 20 - func init() { 21 - _ = prometheus.Register(readTimes) 22 - } 23 - 24 - func handlePageViewTimer(w http.ResponseWriter, r *http.Request) { 25 - if r.Header.Get("DNT") == "1" { 26 - http.NotFound(w, r) 27 - return 28 - } 29 - 30 - data, err := ioutil.ReadAll(r.Body) 31 - if err != nil { 32 - ln.Error(r.Context(), err, ln.Info("while reading data")) 33 - http.Error(w, "oopsie whoopsie uwu", http.StatusInternalServerError) 34 - return 35 - } 36 - r.Body.Close() 37 - 38 - type metricsData struct { 39 - Path string `json:"path"` 40 - StartTime time.Time `json:"start_time"` 41 - EndTime time.Time `json:"end_time"` 42 - } 43 - var md metricsData 44 - err = json.Unmarshal(data, &md) 45 - if err != nil { 46 - http.NotFound(w, r) 47 - return 48 - } 49 - 50 - diff := md.EndTime.Sub(md.StartTime).Seconds() 51 - 52 - readTimes.WithLabelValues(md.Path).Observe(float64(diff)) 53 - }
-112
cmd/site/patreon.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "net/http" 7 - "os" 8 - "sort" 9 - "time" 10 - 11 - "github.com/mxpv/patreon-go" 12 - "golang.org/x/oauth2" 13 - "within.website/ln" 14 - ) 15 - 16 - func NewPatreonClient() (*patreon.Client, error) { 17 - for _, name := range []string{"CLIENT_ID", "CLIENT_SECRET", "ACCESS_TOKEN", "REFRESH_TOKEN"} { 18 - if os.Getenv("PATREON_"+name) == "" { 19 - return nil, fmt.Errorf("wanted envvar PATREON_%s", name) 20 - } 21 - } 22 - 23 - config := oauth2.Config{ 24 - ClientID: os.Getenv("PATREON_CLIENT_ID"), 25 - ClientSecret: os.Getenv("PATREON_CLIENT_SECRET"), 26 - Endpoint: oauth2.Endpoint{ 27 - AuthURL: patreon.AuthorizationURL, 28 - TokenURL: patreon.AccessTokenURL, 29 - }, 30 - Scopes: []string{"users", "campaigns", "pledges", "pledges-to-me", "my-campaign"}, 31 - } 32 - 33 - token := oauth2.Token{ 34 - AccessToken: os.Getenv("PATREON_ACCESS_TOKEN"), 35 - RefreshToken: os.Getenv("PATREON_REFRESH_TOKEN"), 36 - // Must be non-nil, otherwise token will not be expired 37 - Expiry: time.Now().Add(90 * 24 * time.Hour), 38 - } 39 - 40 - tc := config.Client(context.Background(), &token) 41 - 42 - trans := tc.Transport 43 - tc.Transport = lnLoggingTransport{next: trans} 44 - client := patreon.NewClient(tc) 45 - 46 - return client, nil 47 - } 48 - 49 - func GetPledges(pc *patreon.Client) ([]string, error) { 50 - campaign, err := pc.FetchCampaign() 51 - if err != nil { 52 - return nil, fmt.Errorf("campaign fetch error: %w", err) 53 - } 54 - 55 - campaignID := campaign.Data[0].ID 56 - 57 - cursor := "" 58 - var result []string 59 - 60 - for { 61 - pledgesResponse, err := pc.FetchPledges(campaignID, patreon.WithPageSize(25), patreon.WithCursor(cursor)) 62 - if err != nil { 63 - return nil, err 64 - } 65 - 66 - users := make(map[string]*patreon.User) 67 - for _, item := range pledgesResponse.Included.Items { 68 - u, ok := item.(*patreon.User) 69 - if !ok { 70 - continue 71 - } 72 - 73 - users[u.ID] = u 74 - } 75 - 76 - for _, pledge := range pledgesResponse.Data { 77 - pid := pledge.Relationships.Patron.Data.ID 78 - patronFullName := users[pid].Attributes.FullName 79 - 80 - result = append(result, patronFullName) 81 - } 82 - 83 - cursor = pledgesResponse.Links.Next 84 - if cursor == "" { 85 - break 86 - } 87 - } 88 - 89 - sort.Strings(result) 90 - return result, nil 91 - } 92 - 93 - type lnLoggingTransport struct{ next http.RoundTripper } 94 - 95 - func (l lnLoggingTransport) RoundTrip(r *http.Request) (*http.Response, error) { 96 - ctx := r.Context() 97 - f := ln.F{ 98 - "url": r.URL.String(), 99 - "has_token": r.Header.Get("Authorization") != "", 100 - } 101 - 102 - resp, err := l.next.RoundTrip(r) 103 - if err != nil { 104 - return nil, err 105 - } 106 - 107 - f["status"] = resp.Status 108 - 109 - ln.Log(ctx, f) 110 - 111 - return resp, nil 112 - }
-91
cmd/site/rss.go
··· 1 - package main 2 - 3 - import ( 4 - "encoding/json" 5 - "net/http" 6 - "time" 7 - 8 - "christine.website/cmd/site/internal" 9 - "within.website/ln" 10 - "within.website/ln/opname" 11 - ) 12 - 13 - var bootTime = time.Now() 14 - var etag = internal.Hash(bootTime.String(), IncrediblySecureSalt) 15 - 16 - // IncrediblySecureSalt ******* 17 - const IncrediblySecureSalt = "hunter2" 18 - 19 - func (s *Site) createFeed(w http.ResponseWriter, r *http.Request) { 20 - ctx := opname.With(r.Context(), "rss-feed") 21 - fetag := "W/" + internal.Hash(bootTime.String(), IncrediblySecureSalt) 22 - w.Header().Set("ETag", fetag) 23 - 24 - if r.Header.Get("If-None-Match") == fetag { 25 - http.Error(w, "Cached data OK", http.StatusNotModified) 26 - ln.Log(ctx, ln.Info("cache hit")) 27 - return 28 - } 29 - 30 - w.Header().Set("Content-Type", "application/rss+xml") 31 - err := s.rssFeed.WriteRss(w) 32 - if err != nil { 33 - http.Error(w, "Internal server error", http.StatusInternalServerError) 34 - ln.Error(r.Context(), err, ln.F{ 35 - "remote_addr": r.RemoteAddr, 36 - "action": "generating_rss", 37 - "uri": r.RequestURI, 38 - "host": r.Host, 39 - }) 40 - } 41 - } 42 - 43 - func (s *Site) createAtom(w http.ResponseWriter, r *http.Request) { 44 - ctx := opname.With(r.Context(), "atom-feed") 45 - fetag := "W/" + internal.Hash(bootTime.String(), IncrediblySecureSalt) 46 - w.Header().Set("ETag", fetag) 47 - 48 - if r.Header.Get("If-None-Match") == fetag { 49 - http.Error(w, "Cached data OK", http.StatusNotModified) 50 - ln.Log(ctx, ln.Info("cache hit")) 51 - return 52 - } 53 - 54 - w.Header().Set("Content-Type", "application/atom+xml") 55 - err := s.rssFeed.WriteAtom(w) 56 - if err != nil { 57 - http.Error(w, "Internal server error", http.StatusInternalServerError) 58 - ln.Error(ctx, err, ln.F{ 59 - "remote_addr": r.RemoteAddr, 60 - "action": "generating_atom", 61 - "uri": r.RequestURI, 62 - "host": r.Host, 63 - }) 64 - } 65 - } 66 - 67 - func (s *Site) createJSONFeed(w http.ResponseWriter, r *http.Request) { 68 - ctx := opname.With(r.Context(), "atom-feed") 69 - fetag := "W/" + internal.Hash(bootTime.String(), IncrediblySecureSalt) 70 - w.Header().Set("ETag", fetag) 71 - 72 - if r.Header.Get("If-None-Match") == fetag { 73 - http.Error(w, "Cached data OK", http.StatusNotModified) 74 - ln.Log(ctx, ln.Info("cache hit")) 75 - return 76 - } 77 - 78 - w.Header().Set("Content-Type", "application/json") 79 - e := json.NewEncoder(w) 80 - e.SetIndent("", "\t") 81 - err := e.Encode(s.jsonFeed) 82 - if err != nil { 83 - http.Error(w, "Internal server error", http.StatusInternalServerError) 84 - ln.Error(ctx, err, ln.F{ 85 - "remote_addr": r.RemoteAddr, 86 - "action": "generating_jsonfeed", 87 - "uri": r.RequestURI, 88 - "host": r.Host, 89 - }) 90 - } 91 - }
-29
cmd/site/signalboost.go
··· 1 - package main 2 - 3 - import ( 4 - "io/ioutil" 5 - 6 - "github.com/philandstuff/dhall-golang" 7 - ) 8 - 9 - type Person struct { 10 - Name string `dhall:"name"` 11 - GitLink string `dhall:"gitLink"` 12 - Twitter string `dhall:"twitter"` 13 - Tags []string `dhall:"tags"` 14 - } 15 - 16 - func loadPeople(path string) ([]Person, error) { 17 - data, err := ioutil.ReadFile(path) 18 - if err != nil { 19 - return nil, err 20 - } 21 - 22 - var people []Person 23 - err = dhall.Unmarshal(data, &people) 24 - if err != nil { 25 - return nil, err 26 - } 27 - 28 - return people, nil 29 - }
-28
cmd/site/signalboost_test.go
··· 1 - package main 2 - 3 - import "testing" 4 - 5 - func TestLoadPeople(t *testing.T) { 6 - people, err := loadPeople("../../signalboost.dhall") 7 - if err != nil {t.Fatal(err)} 8 - 9 - for _, person := range people { 10 - t.Run(person.Name, func(t *testing.T) { 11 - if person.Name == "" { 12 - t.Error("missing name") 13 - } 14 - 15 - if len(person.Tags) == 0 { 16 - t.Error("missing tags") 17 - } 18 - 19 - if person.Twitter == "" { 20 - t.Error("missing twitter") 21 - } 22 - 23 - if person.GitLink == "" { 24 - t.Error("missing git link") 25 - } 26 - }) 27 - } 28 - }
+27
config.dhall
··· 1 + let Person = 2 + { Type = { name : Text, tags : List Text, gitLink : Text, twitter : Text } 3 + , default = 4 + { name = "", tags = [] : List Text, gitLink = "", twitter = "" } 5 + } 6 + 7 + let defaultPort = env:PORT ? 3030 8 + 9 + let Config = 10 + { Type = 11 + { signalboost : List Person.Type 12 + , port : Natural 13 + , clackSet : List Text 14 + , resumeFname : Text 15 + } 16 + , default = 17 + { signalboost = [] : List Person.Type 18 + , port = defaultPort 19 + , clackSet = [ "Ashlynn" ] 20 + , resumeFname = "./static/resume/resume.md" 21 + } 22 + } 23 + 24 + in Config::{ 25 + , signalboost = ./signalboost.dhall 26 + , clackSet = [ "Ashlynn", "Terry Davis", "Dennis Ritchie" ] 27 + }
+20
css/shim.css
··· 1 + .main { 2 + padding: 20px 10px; 3 + } 4 + 5 + .hack h1 { 6 + padding-top: 0; 7 + } 8 + 9 + footer.footer { 10 + border-top: 1px solid #ccc; 11 + margin-top: 80px; 12 + margin-top: 5rem; 13 + padding: 48px 0; 14 + padding: 3rem 0; 15 + } 16 + 17 + img { 18 + max-width: 100%; 19 + padding: 1em; 20 + }
+20 -3
default.nix
··· 1 - { }: 1 + { system ? builtins.currentSystem }: 2 2 3 3 let 4 4 sources = import ./nix/sources.nix; 5 - pkgs = import sources.nixpkgs { }; 6 - in pkgs.callPackage ./site.nix { inherit pkgs; } 5 + pkgs = import sources.nixpkgs { inherit system; }; 6 + callPackage = pkgs.lib.callPackageWith pkgs; 7 + site = callPackage ./site.nix { }; 8 + 9 + dockerImage = pkg: 10 + pkgs.dockerTools.buildLayeredImage { 11 + name = "xena/christinewebsite"; 12 + tag = "latest"; 13 + 14 + contents = [ pkgs.cacert pkg ]; 15 + 16 + config = { 17 + Cmd = [ "${pkg}/bin/xesite" ]; 18 + Env = [ "CONFIG_FNAME=${pkg}/config.dhall" "RUST_LOG=info" ]; 19 + WorkingDir = "/"; 20 + }; 21 + }; 22 + 23 + in dockerImage site
-21
docker.nix
··· 1 - { system ? builtins.currentSystem }: 2 - 3 - let 4 - pkgs = import (import ./nix/sources.nix).nixpkgs { inherit system; }; 5 - callPackage = pkgs.lib.callPackageWith pkgs; 6 - site = callPackage ./site.nix { }; 7 - 8 - dockerImage = pkg: 9 - pkgs.dockerTools.buildLayeredImage { 10 - name = "xena/christinewebsite"; 11 - tag = pkg.version; 12 - 13 - contents = [ pkg pkgs.cacert ]; 14 - 15 - config = { 16 - Cmd = [ "/bin/site" ]; 17 - WorkingDir = "/"; 18 - }; 19 - }; 20 - 21 - in dockerImage site
-21
go.mod
··· 1 - module christine.website 2 - 3 - require ( 4 - github.com/celrenheit/sandflake v0.0.0-20190410195419-50a943690bc2 5 - github.com/gorilla/feeds v1.1.1 6 - github.com/joho/godotenv v1.3.0 7 - github.com/mxpv/patreon-go v0.0.0-20190917022727-646111f1d983 8 - github.com/philandstuff/dhall-golang v1.0.0 9 - github.com/povilasv/prommod v0.0.12 10 - github.com/prometheus/client_golang v1.7.1 11 - github.com/russross/blackfriday v2.0.0+incompatible 12 - github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 13 - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 14 - github.com/snabb/sitemap v1.0.0 15 - github.com/stretchr/testify v1.6.1 16 - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d 17 - gopkg.in/yaml.v2 v2.3.0 18 - within.website/ln v0.9.1 19 - ) 20 - 21 - go 1.13
-211
go.sum
··· 1 - cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 - github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 - github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 - github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 - github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 7 - github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= 8 - github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 - github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 - github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 - github.com/celrenheit/sandflake v0.0.0-20190410195419-50a943690bc2 h1:/BpnZPo/sk1vPlt62dLya5KCn7PN9ZBDrpTGlQzgUZI= 12 - github.com/celrenheit/sandflake v0.0.0-20190410195419-50a943690bc2/go.mod h1:7L8gY0+4GYeBc9TvqVuDUq7tXuM6Sj7llnt7HkVwWlQ= 13 - github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 14 - github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 15 - github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 - github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 - github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 - github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 19 - github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 20 - github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 21 - github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 22 - github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 23 - github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 24 - github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 25 - github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 - github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 27 - github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 - github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 29 - github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 - github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 31 - github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 32 - github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 33 - github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 34 - github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= 35 - github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 36 - github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 37 - github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 38 - github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 39 - github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 40 - github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 41 - github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 42 - github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 - github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 44 - github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= 45 - github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= 46 - github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 47 - github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 48 - github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 49 - github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 50 - github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 51 - github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 52 - github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 53 - github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 54 - github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 55 - github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 56 - github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 57 - github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 58 - github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 59 - github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 60 - github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= 61 - github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 62 - github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 63 - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 65 - github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 66 - github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 67 - github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 68 - github.com/mxpv/patreon-go v0.0.0-20190917022727-646111f1d983 h1:r32TFg+FHLnoF8PCqCQNp+R9EjMBuP62FXkD/Eqp9Us= 69 - github.com/mxpv/patreon-go v0.0.0-20190917022727-646111f1d983/go.mod h1:ksYjm2GAbGlgIP7jO9Q5/AdyE4MwwEbgQ+lFMx3hyiM= 70 - github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 71 - github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 72 - github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 73 - github.com/philandstuff/dhall-golang v1.0.0 h1:4iYE+OfVjpXtwB6todsw5w+rnBvAhufgpNzAo9K0ljw= 74 - github.com/philandstuff/dhall-golang v1.0.0/go.mod h1:nYfzcKjqq6UDCStpXV6UxRwD0HX9IK9z/MuHmHghbEY= 75 - github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 76 - github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 77 - github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 - github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 - github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 - github.com/povilasv/prommod v0.0.12 h1:0bk9QJ7kD6SmSsk9MeHhz5Qe6OpQl11Fvo7cvvmNUQM= 81 - github.com/povilasv/prommod v0.0.12/go.mod h1:GnuK7wLoVBwZXj8bhbJNx/xFSldy7Q49A44RJKNM8XQ= 82 - github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 83 - github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= 84 - github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 85 - github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8= 86 - github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 87 - github.com/prometheus/client_golang v1.5.0 h1:Ctq0iGpCmr3jeP77kbF2UxgvRwzWWz+4Bh9/vJTyg1A= 88 - github.com/prometheus/client_golang v1.5.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 89 - github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= 90 - github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 91 - github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A= 92 - github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= 93 - github.com/prometheus/client_golang v1.7.0 h1:wCi7urQOGBsYcQROHqpUUX4ct84xp40t9R9JX0FuA/U= 94 - github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 95 - github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= 96 - github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 97 - github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 98 - github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= 99 - github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 100 - github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 101 - github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 102 - github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= 103 - github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 104 - github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= 105 - github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 106 - github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= 107 - github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 108 - github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 109 - github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= 110 - github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 111 - github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= 112 - github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 113 - github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= 114 - github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 115 - github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= 116 - github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 117 - github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= 118 - github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 119 - github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 h1:eajwn6K3weW5cd1ZXLu2sJ4pvwlBiCWY4uDejOr73gM= 120 - github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0= 121 - github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 122 - github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 123 - github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 124 - github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 125 - github.com/snabb/diagio v1.0.0 h1:kovhQ1rDXoEbmpf/T5N2sUp2iOdxEg+TcqzbYVHV2V0= 126 - github.com/snabb/diagio v1.0.0/go.mod h1:ZyGaWFhfBVqstGUw6laYetzeTwZ2xxVPqTALx1QQa1w= 127 - github.com/snabb/sitemap v1.0.0 h1:7vJeNPAaaj7fQSRS3WYuJHzUjdnhLdSLLpvVtnhbzC0= 128 - github.com/snabb/sitemap v1.0.0/go.mod h1:Id8uz1+WYdiNmSjEi4BIvL5UwNPYLsTHzRbjmDwNDzA= 129 - github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 130 - github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 131 - github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 132 - github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 133 - github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 134 - github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 135 - github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 136 - github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 137 - github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 138 - github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 139 - github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= 140 - github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 141 - github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 142 - github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 143 - github.com/ugorji/go v1.1.5-0.20190603013658-a2c9fa250719 h1:UW5IeyWBDAPQ+Qu1hT/lwtxL7pP3L+ETA8WuBvvvBWU= 144 - github.com/ugorji/go v1.1.5-0.20190603013658-a2c9fa250719/go.mod h1:RaaajvHwnCbhlqWLTIB78hyPWp24YUXhQ3YXM7Hg7os= 145 - golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 146 - golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 147 - golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 148 - golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 149 - golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 150 - golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 151 - golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 152 - golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 153 - golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= 154 - golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 155 - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 156 - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 157 - golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 - golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 - golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 - golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 161 - golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 162 - golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 163 - golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 164 - golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 165 - golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 166 - golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 - golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 - golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 - golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= 170 - golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 - golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= 172 - golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 - golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= 174 - golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 - golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 176 - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 177 - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 178 - google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 179 - google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 180 - google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 181 - google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 182 - google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 183 - google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 184 - google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= 185 - google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 186 - google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 187 - google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 188 - gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 189 - gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 190 - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 191 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 192 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 193 - gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 194 - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 195 - gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 196 - gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 197 - gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 198 - gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 199 - gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 200 - gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 201 - gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 202 - gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 203 - gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 204 - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 205 - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 206 - within.website/ln v0.8.0 h1:NX6Eo3LkM9RU8lLRbWpmR5/jQRYKtZ8zuiYi7mmKa6w= 207 - within.website/ln v0.8.0/go.mod h1:I+Apk8qxMStNXTZdyDMqDqe6CB8Hn6+W/Gyf5QbY+2E= 208 - within.website/ln v0.9.0 h1:165zpOgw5Rq278x+u2j3o4662BW/pjavL0vsAzyumxk= 209 - within.website/ln v0.9.0/go.mod h1:I+Apk8qxMStNXTZdyDMqDqe6CB8Hn6+W/Gyf5QbY+2E= 210 - within.website/ln v0.9.1 h1:Qi8IjeCnU43jXijKtr5qtcbjuiCVAudOIxqTim7svnc= 211 - within.website/ln v0.9.1/go.mod h1:I+Apk8qxMStNXTZdyDMqDqe6CB8Hn6+W/Gyf5QbY+2E=
-17
jsonfeed/.travis.yml
··· 1 - # This Source Code Form is subject to the terms of the Mozilla Public 2 - # License, v. 2.0. If a copy of the MPL was not distributed with this 3 - # file, You can obtain one at http://mozilla.org/MPL/2.0/ 4 - 5 - language: go 6 - 7 - go: 8 - - 1.8.1 9 - 10 - before_install: 11 - - go get -t -v ./... 12 - 13 - script: 14 - - go test -race -coverprofile=coverage.txt -covermode=atomic 15 - 16 - after_success: 17 - - bash <(curl -s https://codecov.io/bash)
-363
jsonfeed/LICENSE
··· 1 - Mozilla Public License, version 2.0 2 - 3 - 1. Definitions 4 - 5 - 1.1. "Contributor" 6 - 7 - means each individual or legal entity that creates, contributes to the 8 - creation of, or owns Covered Software. 9 - 10 - 1.2. "Contributor Version" 11 - 12 - means the combination of the Contributions of others (if any) used by a 13 - Contributor and that particular Contributor's Contribution. 14 - 15 - 1.3. "Contribution" 16 - 17 - means Covered Software of a particular Contributor. 18 - 19 - 1.4. "Covered Software" 20 - 21 - means Source Code Form to which the initial Contributor has attached the 22 - notice in Exhibit A, the Executable Form of such Source Code Form, and 23 - Modifications of such Source Code Form, in each case including portions 24 - thereof. 25 - 26 - 1.5. "Incompatible With Secondary Licenses" 27 - means 28 - 29 - a. that the initial Contributor has attached the notice described in 30 - Exhibit B to the Covered Software; or 31 - 32 - b. that the Covered Software was made available under the terms of 33 - version 1.1 or earlier of the License, but not also under the terms of 34 - a Secondary License. 35 - 36 - 1.6. "Executable Form" 37 - 38 - means any form of the work other than Source Code Form. 39 - 40 - 1.7. "Larger Work" 41 - 42 - means a work that combines Covered Software with other material, in a 43 - separate file or files, that is not Covered Software. 44 - 45 - 1.8. "License" 46 - 47 - means this document. 48 - 49 - 1.9. "Licensable" 50 - 51 - means having the right to grant, to the maximum extent possible, whether 52 - at the time of the initial grant or subsequently, any and all of the 53 - rights conveyed by this License. 54 - 55 - 1.10. "Modifications" 56 - 57 - means any of the following: 58 - 59 - a. any file in Source Code Form that results from an addition to, 60 - deletion from, or modification of the contents of Covered Software; or 61 - 62 - b. any new file in Source Code Form that contains any Covered Software. 63 - 64 - 1.11. "Patent Claims" of a Contributor 65 - 66 - means any patent claim(s), including without limitation, method, 67 - process, and apparatus claims, in any patent Licensable by such 68 - Contributor that would be infringed, but for the grant of the License, 69 - by the making, using, selling, offering for sale, having made, import, 70 - or transfer of either its Contributions or its Contributor Version. 71 - 72 - 1.12. "Secondary License" 73 - 74 - means either the GNU General Public License, Version 2.0, the GNU Lesser 75 - General Public License, Version 2.1, the GNU Affero General Public 76 - License, Version 3.0, or any later versions of those licenses. 77 - 78 - 1.13. "Source Code Form" 79 - 80 - means the form of the work preferred for making modifications. 81 - 82 - 1.14. "You" (or "Your") 83 - 84 - means an individual or a legal entity exercising rights under this 85 - License. For legal entities, "You" includes any entity that controls, is 86 - controlled by, or is under common control with You. For purposes of this 87 - definition, "control" means (a) the power, direct or indirect, to cause 88 - the direction or management of such entity, whether by contract or 89 - otherwise, or (b) ownership of more than fifty percent (50%) of the 90 - outstanding shares or beneficial ownership of such entity. 91 - 92 - 93 - 2. License Grants and Conditions 94 - 95 - 2.1. Grants 96 - 97 - Each Contributor hereby grants You a world-wide, royalty-free, 98 - non-exclusive license: 99 - 100 - a. under intellectual property rights (other than patent or trademark) 101 - Licensable by such Contributor to use, reproduce, make available, 102 - modify, display, perform, distribute, and otherwise exploit its 103 - Contributions, either on an unmodified basis, with Modifications, or 104 - as part of a Larger Work; and 105 - 106 - b. under Patent Claims of such Contributor to make, use, sell, offer for 107 - sale, have made, import, and otherwise transfer either its 108 - Contributions or its Contributor Version. 109 - 110 - 2.2. Effective Date 111 - 112 - The licenses granted in Section 2.1 with respect to any Contribution 113 - become effective for each Contribution on the date the Contributor first 114 - distributes such Contribution. 115 - 116 - 2.3. Limitations on Grant Scope 117 - 118 - The licenses granted in this Section 2 are the only rights granted under 119 - this License. No additional rights or licenses will be implied from the 120 - distribution or licensing of Covered Software under this License. 121 - Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 - Contributor: 123 - 124 - a. for any code that a Contributor has removed from Covered Software; or 125 - 126 - b. for infringements caused by: (i) Your and any other third party's 127 - modifications of Covered Software, or (ii) the combination of its 128 - Contributions with other software (except as part of its Contributor 129 - Version); or 130 - 131 - c. under Patent Claims infringed by Covered Software in the absence of 132 - its Contributions. 133 - 134 - This License does not grant any rights in the trademarks, service marks, 135 - or logos of any Contributor (except as may be necessary to comply with 136 - the notice requirements in Section 3.4). 137 - 138 - 2.4. Subsequent Licenses 139 - 140 - No Contributor makes additional grants as a result of Your choice to 141 - distribute the Covered Software under a subsequent version of this 142 - License (see Section 10.2) or under the terms of a Secondary License (if 143 - permitted under the terms of Section 3.3). 144 - 145 - 2.5. Representation 146 - 147 - Each Contributor represents that the Contributor believes its 148 - Contributions are its original creation(s) or it has sufficient rights to 149 - grant the rights to its Contributions conveyed by this License. 150 - 151 - 2.6. Fair Use 152 - 153 - This License is not intended to limit any rights You have under 154 - applicable copyright doctrines of fair use, fair dealing, or other 155 - equivalents. 156 - 157 - 2.7. Conditions 158 - 159 - Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 - Section 2.1. 161 - 162 - 163 - 3. Responsibilities 164 - 165 - 3.1. Distribution of Source Form 166 - 167 - All distribution of Covered Software in Source Code Form, including any 168 - Modifications that You create or to which You contribute, must be under 169 - the terms of this License. You must inform recipients that the Source 170 - Code Form of the Covered Software is governed by the terms of this 171 - License, and how they can obtain a copy of this License. You may not 172 - attempt to alter or restrict the recipients' rights in the Source Code 173 - Form. 174 - 175 - 3.2. Distribution of Executable Form 176 - 177 - If You distribute Covered Software in Executable Form then: 178 - 179 - a. such Covered Software must also be made available in Source Code Form, 180 - as described in Section 3.1, and You must inform recipients of the 181 - Executable Form how they can obtain a copy of such Source Code Form by 182 - reasonable means in a timely manner, at a charge no more than the cost 183 - of distribution to the recipient; and 184 - 185 - b. You may distribute such Executable Form under the terms of this 186 - License, or sublicense it under different terms, provided that the 187 - license for the Executable Form does not attempt to limit or alter the 188 - recipients' rights in the Source Code Form under this License. 189 - 190 - 3.3. Distribution of a Larger Work 191 - 192 - You may create and distribute a Larger Work under terms of Your choice, 193 - provided that You also comply with the requirements of this License for 194 - the Covered Software. If the Larger Work is a combination of Covered 195 - Software with a work governed by one or more Secondary Licenses, and the 196 - Covered Software is not Incompatible With Secondary Licenses, this 197 - License permits You to additionally distribute such Covered Software 198 - under the terms of such Secondary License(s), so that the recipient of 199 - the Larger Work may, at their option, further distribute the Covered 200 - Software under the terms of either this License or such Secondary 201 - License(s). 202 - 203 - 3.4. Notices 204 - 205 - You may not remove or alter the substance of any license notices 206 - (including copyright notices, patent notices, disclaimers of warranty, or 207 - limitations of liability) contained within the Source Code Form of the 208 - Covered Software, except that You may alter any license notices to the 209 - extent required to remedy known factual inaccuracies. 210 - 211 - 3.5. Application of Additional Terms 212 - 213 - You may choose to offer, and to charge a fee for, warranty, support, 214 - indemnity or liability obligations to one or more recipients of Covered 215 - Software. However, You may do so only on Your own behalf, and not on 216 - behalf of any Contributor. You must make it absolutely clear that any 217 - such warranty, support, indemnity, or liability obligation is offered by 218 - You alone, and You hereby agree to indemnify every Contributor for any 219 - liability incurred by such Contributor as a result of warranty, support, 220 - indemnity or liability terms You offer. You may include additional 221 - disclaimers of warranty and limitations of liability specific to any 222 - jurisdiction. 223 - 224 - 4. Inability to Comply Due to Statute or Regulation 225 - 226 - If it is impossible for You to comply with any of the terms of this License 227 - with respect to some or all of the Covered Software due to statute, 228 - judicial order, or regulation then You must: (a) comply with the terms of 229 - this License to the maximum extent possible; and (b) describe the 230 - limitations and the code they affect. Such description must be placed in a 231 - text file included with all distributions of the Covered Software under 232 - this License. Except to the extent prohibited by statute or regulation, 233 - such description must be sufficiently detailed for a recipient of ordinary 234 - skill to be able to understand it. 235 - 236 - 5. Termination 237 - 238 - 5.1. The rights granted under this License will terminate automatically if You 239 - fail to comply with any of its terms. However, if You become compliant, 240 - then the rights granted under this License from a particular Contributor 241 - are reinstated (a) provisionally, unless and until such Contributor 242 - explicitly and finally terminates Your grants, and (b) on an ongoing 243 - basis, if such Contributor fails to notify You of the non-compliance by 244 - some reasonable means prior to 60 days after You have come back into 245 - compliance. Moreover, Your grants from a particular Contributor are 246 - reinstated on an ongoing basis if such Contributor notifies You of the 247 - non-compliance by some reasonable means, this is the first time You have 248 - received notice of non-compliance with this License from such 249 - Contributor, and You become compliant prior to 30 days after Your receipt 250 - of the notice. 251 - 252 - 5.2. If You initiate litigation against any entity by asserting a patent 253 - infringement claim (excluding declaratory judgment actions, 254 - counter-claims, and cross-claims) alleging that a Contributor Version 255 - directly or indirectly infringes any patent, then the rights granted to 256 - You by any and all Contributors for the Covered Software under Section 257 - 2.1 of this License shall terminate. 258 - 259 - 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 - license agreements (excluding distributors and resellers) which have been 261 - validly granted by You or Your distributors under this License prior to 262 - termination shall survive termination. 263 - 264 - 6. Disclaimer of Warranty 265 - 266 - Covered Software is provided under this License on an "as is" basis, 267 - without warranty of any kind, either expressed, implied, or statutory, 268 - including, without limitation, warranties that the Covered Software is free 269 - of defects, merchantable, fit for a particular purpose or non-infringing. 270 - The entire risk as to the quality and performance of the Covered Software 271 - is with You. Should any Covered Software prove defective in any respect, 272 - You (not any Contributor) assume the cost of any necessary servicing, 273 - repair, or correction. This disclaimer of warranty constitutes an essential 274 - part of this License. No use of any Covered Software is authorized under 275 - this License except under this disclaimer. 276 - 277 - 7. Limitation of Liability 278 - 279 - Under no circumstances and under no legal theory, whether tort (including 280 - negligence), contract, or otherwise, shall any Contributor, or anyone who 281 - distributes Covered Software as permitted above, be liable to You for any 282 - direct, indirect, special, incidental, or consequential damages of any 283 - character including, without limitation, damages for lost profits, loss of 284 - goodwill, work stoppage, computer failure or malfunction, or any and all 285 - other commercial damages or losses, even if such party shall have been 286 - informed of the possibility of such damages. This limitation of liability 287 - shall not apply to liability for death or personal injury resulting from 288 - such party's negligence to the extent applicable law prohibits such 289 - limitation. Some jurisdictions do not allow the exclusion or limitation of 290 - incidental or consequential damages, so this exclusion and limitation may 291 - not apply to You. 292 - 293 - 8. Litigation 294 - 295 - Any litigation relating to this License may be brought only in the courts 296 - of a jurisdiction where the defendant maintains its principal place of 297 - business and such litigation shall be governed by laws of that 298 - jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 - in this Section shall prevent a party's ability to bring cross-claims or 300 - counter-claims. 301 - 302 - 9. Miscellaneous 303 - 304 - This License represents the complete agreement concerning the subject 305 - matter hereof. If any provision of this License is held to be 306 - unenforceable, such provision shall be reformed only to the extent 307 - necessary to make it enforceable. Any law or regulation which provides that 308 - the language of a contract shall be construed against the drafter shall not 309 - be used to construe this License against a Contributor. 310 - 311 - 312 - 10. Versions of the License 313 - 314 - 10.1. New Versions 315 - 316 - Mozilla Foundation is the license steward. Except as provided in Section 317 - 10.3, no one other than the license steward has the right to modify or 318 - publish new versions of this License. Each version will be given a 319 - distinguishing version number. 320 - 321 - 10.2. Effect of New Versions 322 - 323 - You may distribute the Covered Software under the terms of the version 324 - of the License under which You originally received the Covered Software, 325 - or under the terms of any subsequent version published by the license 326 - steward. 327 - 328 - 10.3. Modified Versions 329 - 330 - If you create software not governed by this License, and you want to 331 - create a new license for such software, you may create and use a 332 - modified version of this License if you rename the license and remove 333 - any references to the name of the license steward (except to note that 334 - such modified license differs from this License). 335 - 336 - 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 - Licenses If You choose to distribute Source Code Form that is 338 - Incompatible With Secondary Licenses under the terms of this version of 339 - the License, the notice described in Exhibit B of this License must be 340 - attached. 341 - 342 - Exhibit A - Source Code Form License Notice 343 - 344 - This Source Code Form is subject to the 345 - terms of the Mozilla Public License, v. 346 - 2.0. If a copy of the MPL was not 347 - distributed with this file, You can 348 - obtain one at 349 - http://mozilla.org/MPL/2.0/. 350 - 351 - If it is not possible or desirable to put the notice in a particular file, 352 - then You may include the notice in a location (such as a LICENSE file in a 353 - relevant directory) where a recipient would be likely to look for such a 354 - notice. 355 - 356 - You may add additional accurate notices of copyright ownership. 357 - 358 - Exhibit B - "Incompatible With Secondary Licenses" Notice 359 - 360 - This Source Code Form is "Incompatible 361 - With Secondary Licenses", as defined by 362 - the Mozilla Public License, v. 2.0. 363 -
-8
jsonfeed/README.md
··· 1 - # JSONFeed - Go Package to parse JSON Feed streams 2 - 3 - [![Build Status](https://travis-ci.org/st3fan/jsonfeed.svg?branch=master)](https://travis-ci.org/st3fan/jsonfeed) [![Go Report Card](https://goreportcard.com/badge/github.com/st3fan/jsonfeed)](https://goreportcard.com/report/github.com/st3fan/jsonfeed) [![codecov](https://codecov.io/gh/st3fan/jsonfeed/branch/master/graph/badge.svg)](https://codecov.io/gh/st3fan/jsonfeed) 4 - 5 - 6 - *Stefan Arentz, May 2017* 7 - 8 - Work in progress. Minimal package to parse JSON Feed streams. Please file feature requests.
-73
jsonfeed/jsonfeed.go
··· 1 - // This Source Code Form is subject to the terms of the Mozilla Public 2 - // License, v. 2.0. If a copy of the MPL was not distributed with this 3 - // file, You can obtain one at http://mozilla.org/MPL/2.0/ 4 - 5 - package jsonfeed 6 - 7 - import ( 8 - "encoding/json" 9 - "io" 10 - "time" 11 - ) 12 - 13 - const CurrentVersion = "https://jsonfeed.org/version/1" 14 - 15 - type Item struct { 16 - ID string `json:"id"` 17 - URL string `json:"url"` 18 - ExternalURL string `json:"external_url"` 19 - Title string `json:"title"` 20 - ContentHTML string `json:"content_html"` 21 - ContentText string `json:"content_text"` 22 - Summary string `json:"summary"` 23 - Image string `json:"image"` 24 - BannerImage string `json:"banner_image"` 25 - DatePublished time.Time `json:"date_published"` 26 - DateModified time.Time `json:"date_modified"` 27 - Author Author `json:"author"` 28 - Tags []string `json:"tags"` 29 - } 30 - 31 - type Author struct { 32 - Name string `json:"name"` 33 - URL string `json:"url"` 34 - Avatar string `json:"avatar"` 35 - } 36 - 37 - type Hub struct { 38 - Type string `json:"type"` 39 - URL string `json:"url"` 40 - } 41 - 42 - type Attachment struct { 43 - URL string `json:"url"` 44 - MIMEType string `json:"mime_type"` 45 - Title string `json:"title"` 46 - SizeInBytes int64 `json:"size_in_bytes"` 47 - DurationInSeconds int64 `json:"duration_in_seconds"` 48 - } 49 - 50 - type Feed struct { 51 - Version string `json:"version"` 52 - Title string `json:"title"` 53 - HomePageURL string `json:"home_page_url"` 54 - FeedURL string `json:"feed_url"` 55 - Description string `json:"description"` 56 - UserComment string `json:"user_comment"` 57 - NextURL string `json:"next_url"` 58 - Icon string `json:"icon"` 59 - Favicon string `json:"favicon"` 60 - Author Author `json:"author"` 61 - Expired bool `json:"expired"` 62 - Hubs []Hub `json:"hubs"` 63 - Items []Item `json:"items"` 64 - } 65 - 66 - func Parse(r io.Reader) (Feed, error) { 67 - var feed Feed 68 - decoder := json.NewDecoder(r) 69 - if err := decoder.Decode(&feed); err != nil { 70 - return Feed{}, err 71 - } 72 - return feed, nil 73 - }
-42
jsonfeed/jsonfeed_test.go
··· 1 - // This Source Code Form is subject to the terms of the Mozilla Public 2 - // License, v. 2.0. If a copy of the MPL was not distributed with this 3 - // file, You can obtain one at http://mozilla.org/MPL/2.0/ 4 - 5 - package jsonfeed 6 - 7 - import ( 8 - "os" 9 - "testing" 10 - "time" 11 - 12 - "github.com/stretchr/testify/assert" 13 - ) 14 - 15 - func TestParseSimple(t *testing.T) { 16 - r, err := os.Open("testdata/feed.json") 17 - assert.NoError(t, err, "Could not open testdata/feed.json") 18 - 19 - feed, err := Parse(r) 20 - assert.NoError(t, err, "Could not parse testdata/feed.json") 21 - 22 - assert.Equal(t, "https://jsonfeed.org/version/1", feed.Version) 23 - assert.Equal(t, "JSON Feed", feed.Title) 24 - assert.Equal(t, "JSON Feed is a ...", feed.Description) 25 - assert.Equal(t, "https://jsonfeed.org/", feed.HomePageURL) 26 - assert.Equal(t, "https://jsonfeed.org/feed.json", feed.FeedURL) 27 - assert.Equal(t, "This feed allows ...", feed.UserComment) 28 - assert.Equal(t, "https://jsonfeed.org/graphics/icon.png", feed.Favicon) 29 - assert.Equal(t, "Brent Simmons and Manton Reece", feed.Author.Name) 30 - 31 - assert.Equal(t, 1, len(feed.Items)) 32 - 33 - assert.Equal(t, "https://jsonfeed.org/2017/05/17/announcing_json_feed", feed.Items[0].ID) 34 - assert.Equal(t, "https://jsonfeed.org/2017/05/17/announcing_json_feed", feed.Items[0].URL) 35 - assert.Equal(t, "Announcing JSON Feed", feed.Items[0].Title) 36 - assert.Equal(t, "<p>We ...", feed.Items[0].ContentHTML) 37 - 38 - datePublished, err := time.Parse("2006-01-02T15:04:05-07:00", "2017-05-17T08:02:12-07:00") 39 - assert.NoError(t, err, "Could not parse timestamp") 40 - 41 - assert.Equal(t, datePublished, feed.Items[0].DatePublished) 42 - }
-21
jsonfeed/testdata/feed.json
··· 1 - { 2 - "version": "https://jsonfeed.org/version/1", 3 - "title": "JSON Feed", 4 - "description": "JSON Feed is a ...", 5 - "home_page_url": "https://jsonfeed.org/", 6 - "feed_url": "https://jsonfeed.org/feed.json", 7 - "user_comment": "This feed allows ...", 8 - "favicon": "https://jsonfeed.org/graphics/icon.png", 9 - "author": { 10 - "name": "Brent Simmons and Manton Reece" 11 - }, 12 - "items": [ 13 - { 14 - "id": "https://jsonfeed.org/2017/05/17/announcing_json_feed", 15 - "url": "https://jsonfeed.org/2017/05/17/announcing_json_feed", 16 - "title": "Announcing JSON Feed", 17 - "content_html": "<p>We ...", 18 - "date_published": "2017-05-17T08:02:12-07:00" 19 - } 20 - ] 21 - }
+15
lib/go_vanity/Cargo.toml
··· 1 + [package] 2 + name = "go_vanity" 3 + version = "0.1.0" 4 + authors = ["Christine Dodrill <me@christine.website>"] 5 + edition = "2018" 6 + build = "src/build.rs" 7 + 8 + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 + 10 + [dependencies] 11 + mime = "0.3.0" 12 + warp = "0.2" 13 + 14 + [build-dependencies] 15 + ructe = { version = "0.11", features = ["warp02"] }
+5
lib/go_vanity/src/build.rs
··· 1 + use ructe::{Result, Ructe}; 2 + 3 + fn main() -> Result<()> { 4 + Ructe::from_env()?.compile_templates("templates") 5 + }
+12
lib/go_vanity/src/lib.rs
··· 1 + use warp::{http::Response, Rejection, Reply}; 2 + use crate::templates::{RenderRucte}; 3 + 4 + include!(concat!(env!("OUT_DIR"), "/templates.rs")); 5 + 6 + pub async fn gitea(pkg_name: &str, git_repo: &str) -> Result<impl Reply, Rejection> { 7 + Response::builder().html(|o| templates::gitea_html(o, pkg_name, git_repo)) 8 + } 9 + 10 + pub async fn github(pkg_name: &str, git_repo: &str) -> Result<impl Reply, Rejection> { 11 + Response::builder().html(|o| templates::github_html(o, pkg_name, git_repo)) 12 + }
+14
lib/go_vanity/templates/gitea.rs.html
··· 1 + @(pkg_name: &str, git_repo: &str) 2 + 3 + <!DOCTYPE html> 4 + <html> 5 + <head> 6 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 7 + <meta name="go-import" content="@pkg_name git @git_repo"> 8 + <meta name="go-source" content="@pkg_name @git_repo @git_repo/src/master@{/dir@} @git_repo/src/master@{/dir@}/@{file@}#L@{line@}"> 9 + <meta http-equiv="refresh" content="0; url=https://godoc.org/@pkg_name"> 10 + </head> 11 + <body> 12 + Please see <a href="https://godoc.org/@pkg_name">here</a> for documentation on this package. 13 + </body> 14 + </html>
+14
lib/go_vanity/templates/github.rs.html
··· 1 + @(pkg_name: &str, git_repo: &str) 2 + 3 + <!DOCTYPE html> 4 + <html> 5 + <head> 6 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 7 + <meta name="go-import" content="@pkg_name git @git_repo"> 8 + <meta name="go-source" content="@pkg_name @git_repo @git_repo/tree/master@{/dir@} @git_repo/blob/master@{/dir@}/@{file@}#L@{line@}"> 9 + <meta http-equiv="refresh" content="0; url=https://godoc.org/@pkg_name"> 10 + </head> 11 + <body> 12 + Please see <a href="https://godoc.org/@pkg_name">here</a> for documentation on this package. 13 + </body> 14 + </html>
+4
lib/jsonfeed/.gitignore
··· 1 + target/ 2 + **/*.rs.bk 3 + Cargo.lock 4 + *.html
+15
lib/jsonfeed/Cargo.toml
··· 1 + [package] 2 + authors = ["Paul Woolcock <paul@woolcock.us>", "Christine Dodrill <me@christine.website>"] 3 + description = "Parser for the JSONFeed (http://jsonfeed.org) specification\n" 4 + documentation = "https://docs.rs/jsonfeed" 5 + homepage = "https://github.com/pwoolcoc/jsonfeed" 6 + license = "MIT/Apache-2.0" 7 + name = "jsonfeed" 8 + readme = "README.adoc" 9 + version = "0.3.0" 10 + 11 + [dependencies] 12 + error-chain = "0.12" 13 + serde = "1" 14 + serde_derive = "1" 15 + serde_json = "1"
+201
lib/jsonfeed/LICENSE-APACHE
··· 1 + Apache License 2 + Version 2.0, January 2004 3 + http://www.apache.org/licenses/ 4 + 5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 + 7 + 1. Definitions. 8 + 9 + "License" shall mean the terms and conditions for use, reproduction, 10 + and distribution as defined by Sections 1 through 9 of this document. 11 + 12 + "Licensor" shall mean the copyright owner or entity authorized by 13 + the copyright owner that is granting the License. 14 + 15 + "Legal Entity" shall mean the union of the acting entity and all 16 + other entities that control, are controlled by, or are under common 17 + control with that entity. For the purposes of this definition, 18 + "control" means (i) the power, direct or indirect, to cause the 19 + direction or management of such entity, whether by contract or 20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 + outstanding shares, or (iii) beneficial ownership of such entity. 22 + 23 + "You" (or "Your") shall mean an individual or Legal Entity 24 + exercising permissions granted by this License. 25 + 26 + "Source" form shall mean the preferred form for making modifications, 27 + including but not limited to software source code, documentation 28 + source, and configuration files. 29 + 30 + "Object" form shall mean any form resulting from mechanical 31 + transformation or translation of a Source form, including but 32 + not limited to compiled object code, generated documentation, 33 + and conversions to other media types. 34 + 35 + "Work" shall mean the work of authorship, whether in Source or 36 + Object form, made available under the License, as indicated by a 37 + copyright notice that is included in or attached to the work 38 + (an example is provided in the Appendix below). 39 + 40 + "Derivative Works" shall mean any work, whether in Source or Object 41 + form, that is based on (or derived from) the Work and for which the 42 + editorial revisions, annotations, elaborations, or other modifications 43 + represent, as a whole, an original work of authorship. For the purposes 44 + of this License, Derivative Works shall not include works that remain 45 + separable from, or merely link (or bind by name) to the interfaces of, 46 + the Work and Derivative Works thereof. 47 + 48 + "Contribution" shall mean any work of authorship, including 49 + the original version of the Work and any modifications or additions 50 + to that Work or Derivative Works thereof, that is intentionally 51 + submitted to Licensor for inclusion in the Work by the copyright owner 52 + or by an individual or Legal Entity authorized to submit on behalf of 53 + the copyright owner. For the purposes of this definition, "submitted" 54 + means any form of electronic, verbal, or written communication sent 55 + to the Licensor or its representatives, including but not limited to 56 + communication on electronic mailing lists, source code control systems, 57 + and issue tracking systems that are managed by, or on behalf of, the 58 + Licensor for the purpose of discussing and improving the Work, but 59 + excluding communication that is conspicuously marked or otherwise 60 + designated in writing by the copyright owner as "Not a Contribution." 61 + 62 + "Contributor" shall mean Licensor and any individual or Legal Entity 63 + on behalf of whom a Contribution has been received by Licensor and 64 + subsequently incorporated within the Work. 65 + 66 + 2. Grant of Copyright License. Subject to the terms and conditions of 67 + this License, each Contributor hereby grants to You a perpetual, 68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 + copyright license to reproduce, prepare Derivative Works of, 70 + publicly display, publicly perform, sublicense, and distribute the 71 + Work and such Derivative Works in Source or Object form. 72 + 73 + 3. Grant of Patent License. Subject to the terms and conditions of 74 + this License, each Contributor hereby grants to You a perpetual, 75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 + (except as stated in this section) patent license to make, have made, 77 + use, offer to sell, sell, import, and otherwise transfer the Work, 78 + where such license applies only to those patent claims licensable 79 + by such Contributor that are necessarily infringed by their 80 + Contribution(s) alone or by combination of their Contribution(s) 81 + with the Work to which such Contribution(s) was submitted. If You 82 + institute patent litigation against any entity (including a 83 + cross-claim or counterclaim in a lawsuit) alleging that the Work 84 + or a Contribution incorporated within the Work constitutes direct 85 + or contributory patent infringement, then any patent licenses 86 + granted to You under this License for that Work shall terminate 87 + as of the date such litigation is filed. 88 + 89 + 4. Redistribution. You may reproduce and distribute copies of the 90 + Work or Derivative Works thereof in any medium, with or without 91 + modifications, and in Source or Object form, provided that You 92 + meet the following conditions: 93 + 94 + (a) You must give any other recipients of the Work or 95 + Derivative Works a copy of this License; and 96 + 97 + (b) You must cause any modified files to carry prominent notices 98 + stating that You changed the files; and 99 + 100 + (c) You must retain, in the Source form of any Derivative Works 101 + that You distribute, all copyright, patent, trademark, and 102 + attribution notices from the Source form of the Work, 103 + excluding those notices that do not pertain to any part of 104 + the Derivative Works; and 105 + 106 + (d) If the Work includes a "NOTICE" text file as part of its 107 + distribution, then any Derivative Works that You distribute must 108 + include a readable copy of the attribution notices contained 109 + within such NOTICE file, excluding those notices that do not 110 + pertain to any part of the Derivative Works, in at least one 111 + of the following places: within a NOTICE text file distributed 112 + as part of the Derivative Works; within the Source form or 113 + documentation, if provided along with the Derivative Works; or, 114 + within a display generated by the Derivative Works, if and 115 + wherever such third-party notices normally appear. The contents 116 + of the NOTICE file are for informational purposes only and 117 + do not modify the License. You may add Your own attribution 118 + notices within Derivative Works that You distribute, alongside 119 + or as an addendum to the NOTICE text from the Work, provided 120 + that such additional attribution notices cannot be construed 121 + as modifying the License. 122 + 123 + You may add Your own copyright statement to Your modifications and 124 + may provide additional or different license terms and conditions 125 + for use, reproduction, or distribution of Your modifications, or 126 + for any such Derivative Works as a whole, provided Your use, 127 + reproduction, and distribution of the Work otherwise complies with 128 + the conditions stated in this License. 129 + 130 + 5. Submission of Contributions. Unless You explicitly state otherwise, 131 + any Contribution intentionally submitted for inclusion in the Work 132 + by You to the Licensor shall be under the terms and conditions of 133 + this License, without any additional terms or conditions. 134 + Notwithstanding the above, nothing herein shall supersede or modify 135 + the terms of any separate license agreement you may have executed 136 + with Licensor regarding such Contributions. 137 + 138 + 6. Trademarks. This License does not grant permission to use the trade 139 + names, trademarks, service marks, or product names of the Licensor, 140 + except as required for reasonable and customary use in describing the 141 + origin of the Work and reproducing the content of the NOTICE file. 142 + 143 + 7. Disclaimer of Warranty. Unless required by applicable law or 144 + agreed to in writing, Licensor provides the Work (and each 145 + Contributor provides its Contributions) on an "AS IS" BASIS, 146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 + implied, including, without limitation, any warranties or conditions 148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 + PARTICULAR PURPOSE. You are solely responsible for determining the 150 + appropriateness of using or redistributing the Work and assume any 151 + risks associated with Your exercise of permissions under this License. 152 + 153 + 8. Limitation of Liability. In no event and under no legal theory, 154 + whether in tort (including negligence), contract, or otherwise, 155 + unless required by applicable law (such as deliberate and grossly 156 + negligent acts) or agreed to in writing, shall any Contributor be 157 + liable to You for damages, including any direct, indirect, special, 158 + incidental, or consequential damages of any character arising as a 159 + result of this License or out of the use or inability to use the 160 + Work (including but not limited to damages for loss of goodwill, 161 + work stoppage, computer failure or malfunction, or any and all 162 + other commercial damages or losses), even if such Contributor 163 + has been advised of the possibility of such damages. 164 + 165 + 9. Accepting Warranty or Additional Liability. While redistributing 166 + the Work or Derivative Works thereof, You may choose to offer, 167 + and charge a fee for, acceptance of support, warranty, indemnity, 168 + or other liability obligations and/or rights consistent with this 169 + License. However, in accepting such obligations, You may act only 170 + on Your own behalf and on Your sole responsibility, not on behalf 171 + of any other Contributor, and only if You agree to indemnify, 172 + defend, and hold each Contributor harmless for any liability 173 + incurred by, or claims asserted against, such Contributor by reason 174 + of your accepting any such warranty or additional liability. 175 + 176 + END OF TERMS AND CONDITIONS 177 + 178 + APPENDIX: How to apply the Apache License to your work. 179 + 180 + To apply the Apache License to your work, attach the following 181 + boilerplate notice, with the fields enclosed by brackets "[]" 182 + replaced with your own identifying information. (Don't include 183 + the brackets!) The text should be enclosed in the appropriate 184 + comment syntax for the file format. We also recommend that a 185 + file or class name and description of purpose be included on the 186 + same "printed page" as the copyright notice for easier 187 + identification within third-party archives. 188 + 189 + Copyright [yyyy] [name of copyright owner] 190 + 191 + Licensed under the Apache License, Version 2.0 (the "License"); 192 + you may not use this file except in compliance with the License. 193 + You may obtain a copy of the License at 194 + 195 + http://www.apache.org/licenses/LICENSE-2.0 196 + 197 + Unless required by applicable law or agreed to in writing, software 198 + distributed under the License is distributed on an "AS IS" BASIS, 199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 + See the License for the specific language governing permissions and 201 + limitations under the License.
+25
lib/jsonfeed/LICENSE-MIT
··· 1 + Copyright (c) 2014 The Rust Project Developers 2 + 3 + Permission is hereby granted, free of charge, to any 4 + person obtaining a copy of this software and associated 5 + documentation files (the "Software"), to deal in the 6 + Software without restriction, including without 7 + limitation the rights to use, copy, modify, merge, 8 + publish, distribute, sublicense, and/or sell copies of 9 + the Software, and to permit persons to whom the Software 10 + is furnished to do so, subject to the following 11 + conditions: 12 + 13 + The above copyright notice and this permission notice 14 + shall be included in all copies or substantial portions 15 + of the Software. 16 + 17 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 + DEALINGS IN THE SOFTWARE.
+27
lib/jsonfeed/README.adoc
··· 1 + = JSON Feed Parser 2 + 3 + [link=https://github.com/pwoolcoc/jsonfeed] 4 + image::https://img.shields.io/crates/v/jsonfeed.svg[JSON Feed crate version] 5 + 6 + This is a http://jsonfeed.org[JSON Feed] parser in Rust. Just a thin layer on top of `serde`, but it 7 + provides serialization & deserialization, along with a Builder API for constructing feeds. 8 + 9 + Note that this is alpha, I still need to add a lot of tests and a couple more features. 10 + 11 + == Example 12 + 13 + ---- 14 + extern crate jsonfeed; 15 + extern crate reqwest; 16 + 17 + fn main() { 18 + let resp = reqwest::get("https://example.com/feed.json").unwrap(); 19 + let feed = jsonfeed::from_reader(resp).unwrap(); 20 + println!("Feed title is: {}", feed.title); 21 + } 22 + ---- 23 + 24 + TODO: 25 + 26 + * Tests. Lots and lots of tests 27 + * Implement ability to add, serialize, and deserialize custom attributes from the json feed spec
+204
lib/jsonfeed/src/builder.rs
··· 1 + use std::default::Default; 2 + 3 + use errors::*; 4 + use feed::{Feed, Author, Attachment}; 5 + use item::{Content, Item}; 6 + 7 + /// Feed Builder 8 + /// 9 + /// This is used to programmatically build up a Feed object, 10 + /// which can be serialized later into a JSON string 11 + pub struct Builder(Feed); 12 + 13 + impl Builder { 14 + pub fn new() -> Builder { 15 + Builder(Feed::default()) 16 + } 17 + 18 + pub fn title<I: Into<String>>(mut self, t: I) -> Builder { 19 + self.0.title = t.into(); 20 + self 21 + } 22 + 23 + pub fn home_page_url<I: Into<String>>(mut self, url: I) -> Builder { 24 + self.0.home_page_url = Some(url.into()); 25 + self 26 + } 27 + 28 + pub fn feed_url<I: Into<String>>(mut self, url: I) -> Builder { 29 + self.0.feed_url = Some(url.into()); 30 + self 31 + } 32 + 33 + pub fn description<I: Into<String>>(mut self, desc: I) -> Builder { 34 + self.0.description = Some(desc.into()); 35 + self 36 + } 37 + 38 + pub fn user_comment<I: Into<String>>(mut self, cmt: I) -> Builder { 39 + self.0.user_comment = Some(cmt.into()); 40 + self 41 + } 42 + 43 + pub fn next_url<I: Into<String>>(mut self, url: I) -> Builder { 44 + self.0.next_url = Some(url.into()); 45 + self 46 + } 47 + 48 + pub fn icon<I: Into<String>>(mut self, url: I) -> Builder { 49 + self.0.icon = Some(url.into()); 50 + self 51 + } 52 + 53 + pub fn favicon<I: Into<String>>(mut self, url: I) -> Builder { 54 + self.0.favicon = Some(url.into()); 55 + self 56 + } 57 + 58 + pub fn author(mut self, author: Author) -> Builder { 59 + self.0.author = Some(author); 60 + self 61 + } 62 + 63 + pub fn expired(mut self) -> Builder { 64 + self.0.expired = Some(true); 65 + self 66 + } 67 + 68 + pub fn item(mut self, item: Item) -> Builder { 69 + self.0.items.push(item); 70 + self 71 + } 72 + 73 + pub fn build(self) -> Feed { 74 + self.0 75 + } 76 + } 77 + 78 + /// Builder object for an item in a feed 79 + pub struct ItemBuilder { 80 + pub id: Option<String>, 81 + pub url: Option<String>, 82 + pub external_url: Option<String>, 83 + pub title: Option<String>, 84 + pub content: Option<Content>, 85 + pub summary: Option<String>, 86 + pub image: Option<String>, 87 + pub banner_image: Option<String>, 88 + pub date_published: Option<String>, 89 + pub date_modified: Option<String>, 90 + pub author: Option<Author>, 91 + pub tags: Option<Vec<String>>, 92 + pub attachments: Option<Vec<Attachment>>, 93 + } 94 + 95 + impl ItemBuilder { 96 + pub fn new() -> ItemBuilder { 97 + ItemBuilder { 98 + id: None, 99 + url: None, 100 + external_url: None, 101 + title: None, 102 + content: None, 103 + summary: None, 104 + image: None, 105 + banner_image: None, 106 + date_published: None, 107 + date_modified: None, 108 + author: None, 109 + tags: None, 110 + attachments: None, 111 + } 112 + } 113 + 114 + pub fn title<I: Into<String>>(mut self, i: I) -> ItemBuilder { 115 + self.title = Some(i.into()); 116 + self 117 + } 118 + 119 + pub fn image<I: Into<String>>(mut self, i: I) -> ItemBuilder { 120 + self.image = Some(i.into()); 121 + self 122 + } 123 + 124 + pub fn id<I: Into<String>>(mut self, i: I) -> ItemBuilder { 125 + self.id = Some(i.into()); 126 + self 127 + } 128 + 129 + pub fn url<I: Into<String>>(mut self, i: I) -> ItemBuilder { 130 + self.url = Some(i.into()); 131 + self 132 + } 133 + 134 + pub fn external_url<I: Into<String>>(mut self, i: I) -> ItemBuilder { 135 + self.external_url = Some(i.into()); 136 + self 137 + } 138 + 139 + pub fn date_modified<I: Into<String>>(mut self, i: I) -> ItemBuilder { 140 + self.date_modified = Some(i.into()); 141 + self 142 + } 143 + 144 + pub fn date_published<I: Into<String>>(mut self, i: I) -> ItemBuilder { 145 + self.date_published = Some(i.into()); 146 + self 147 + } 148 + 149 + pub fn tags(mut self, tags: Vec<String>) -> ItemBuilder { 150 + self.tags = Some(tags); 151 + self 152 + } 153 + 154 + pub fn author(mut self, who: Author) -> ItemBuilder { 155 + self.author = Some(who); 156 + self 157 + } 158 + 159 + pub fn content_html<I: Into<String>>(mut self, i: I) -> ItemBuilder { 160 + match self.content { 161 + Some(Content::Text(t)) => { 162 + self.content = Some(Content::Both(i.into(), t)); 163 + }, 164 + _ => { 165 + self.content = Some(Content::Html(i.into())); 166 + } 167 + } 168 + self 169 + } 170 + 171 + pub fn content_text<I: Into<String>>(mut self, i: I) -> ItemBuilder { 172 + match self.content { 173 + Some(Content::Html(s)) => { 174 + self.content = Some(Content::Both(s, i.into())); 175 + }, 176 + _ => { 177 + self.content = Some(Content::Text(i.into())); 178 + }, 179 + } 180 + self 181 + } 182 + 183 + pub fn build(self) -> Result<Item> { 184 + if self.id.is_none() || self.content.is_none() { 185 + return Err("missing field 'id' or 'content_*'".into()); 186 + } 187 + Ok(Item { 188 + id: self.id.unwrap(), 189 + url: self.url, 190 + external_url: self.external_url, 191 + title: self.title, 192 + content: self.content.unwrap(), 193 + summary: self.summary, 194 + image: self.image, 195 + banner_image: self.banner_image, 196 + date_published: self.date_published, 197 + date_modified: self.date_modified, 198 + author: self.author, 199 + tags: self.tags, 200 + attachments: self.attachments 201 + }) 202 + } 203 + } 204 +
+7
lib/jsonfeed/src/errors.rs
··· 1 + use serde_json; 2 + error_chain!{ 3 + foreign_links { 4 + Serde(serde_json::Error); 5 + } 6 + } 7 +
+296
lib/jsonfeed/src/feed.rs
··· 1 + use std::default::Default; 2 + 3 + use item::Item; 4 + use builder::Builder; 5 + 6 + const VERSION_1: &'static str = "https://jsonfeed.org/version/1"; 7 + 8 + /// Represents a single feed 9 + /// 10 + /// # Examples 11 + /// 12 + /// ```rust 13 + /// // Serialize a feed object to a JSON string 14 + /// 15 + /// # extern crate jsonfeed; 16 + /// # use std::default::Default; 17 + /// # use jsonfeed::Feed; 18 + /// # fn main() { 19 + /// let feed: Feed = Feed::default(); 20 + /// assert_eq!( 21 + /// jsonfeed::to_string(&feed).unwrap(), 22 + /// "{\"version\":\"https://jsonfeed.org/version/1\",\"title\":\"\",\"items\":[]}" 23 + /// ); 24 + /// # } 25 + /// ``` 26 + /// 27 + /// ```rust 28 + /// // Deserialize a feed objects from a JSON String 29 + /// 30 + /// # extern crate jsonfeed; 31 + /// # use jsonfeed::Feed; 32 + /// # fn main() { 33 + /// let json = "{\"version\":\"https://jsonfeed.org/version/1\",\"title\":\"\",\"items\":[]}"; 34 + /// let feed: Feed = jsonfeed::from_str(&json).unwrap(); 35 + /// assert_eq!( 36 + /// feed, 37 + /// Feed::default() 38 + /// ); 39 + /// # } 40 + /// ``` 41 + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 42 + pub struct Feed { 43 + pub version: String, 44 + pub title: String, 45 + pub items: Vec<Item>, 46 + #[serde(skip_serializing_if = "Option::is_none")] 47 + pub home_page_url: Option<String>, 48 + #[serde(skip_serializing_if = "Option::is_none")] 49 + pub feed_url: Option<String>, 50 + #[serde(skip_serializing_if = "Option::is_none")] 51 + pub description: Option<String>, 52 + #[serde(skip_serializing_if = "Option::is_none")] 53 + pub user_comment: Option<String>, 54 + #[serde(skip_serializing_if = "Option::is_none")] 55 + pub next_url: Option<String>, 56 + #[serde(skip_serializing_if = "Option::is_none")] 57 + pub icon: Option<String>, 58 + #[serde(skip_serializing_if = "Option::is_none")] 59 + pub favicon: Option<String>, 60 + #[serde(skip_serializing_if = "Option::is_none")] 61 + pub author: Option<Author>, 62 + #[serde(skip_serializing_if = "Option::is_none")] 63 + pub expired: Option<bool>, 64 + #[serde(skip_serializing_if = "Option::is_none")] 65 + pub hubs: Option<Vec<Hub>>, 66 + } 67 + 68 + impl Feed { 69 + /// Used to construct a Feed object 70 + pub fn builder() -> Builder { 71 + Builder::new() 72 + } 73 + } 74 + 75 + impl Default for Feed { 76 + fn default() -> Feed { 77 + Feed { 78 + version: VERSION_1.to_string(), 79 + title: "".to_string(), 80 + items: vec![], 81 + home_page_url: None, 82 + feed_url: None, 83 + description: None, 84 + user_comment: None, 85 + next_url: None, 86 + icon: None, 87 + favicon: None, 88 + author: None, 89 + expired: None, 90 + hubs: None, 91 + } 92 + } 93 + } 94 + 95 + /// Represents an `attachment` for an item 96 + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 97 + pub struct Attachment { 98 + url: String, 99 + mime_type: String, 100 + title: Option<String>, 101 + size_in_bytes: Option<u64>, 102 + duration_in_seconds: Option<u64>, 103 + } 104 + 105 + /// Represents an `author` in both a feed and a feed item 106 + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 107 + pub struct Author { 108 + name: Option<String>, 109 + url: Option<String>, 110 + avatar: Option<String>, 111 + } 112 + 113 + impl Author { 114 + pub fn new() -> Author { 115 + Author { 116 + name: None, 117 + url: None, 118 + avatar: None, 119 + } 120 + } 121 + 122 + pub fn name<I: Into<String>>(mut self, name: I) -> Self { 123 + self.name = Some(name.into()); 124 + self 125 + } 126 + 127 + pub fn url<I: Into<String>>(mut self, url: I) -> Self { 128 + self.url = Some(url.into()); 129 + self 130 + } 131 + 132 + pub fn avatar<I: Into<String>>(mut self, avatar: I) -> Self { 133 + self.avatar = Some(avatar.into()); 134 + self 135 + } 136 + } 137 + 138 + /// Represents a `hub` for a feed 139 + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 140 + pub struct Hub { 141 + #[serde(rename = "type")] 142 + type_: String, 143 + url: String, 144 + } 145 + 146 + #[cfg(test)] 147 + mod tests { 148 + use serde_json; 149 + use std::default::Default; 150 + use super::*; 151 + 152 + #[test] 153 + fn serialize_feed() { 154 + let feed = Feed { 155 + version: "https://jsonfeed.org/version/1".to_string(), 156 + title: "some title".to_string(), 157 + items: vec![], 158 + home_page_url: None, 159 + description: None, 160 + expired: Some(true), 161 + ..Default::default() 162 + }; 163 + assert_eq!( 164 + serde_json::to_string(&feed).unwrap(), 165 + r#"{"version":"https://jsonfeed.org/version/1","title":"some title","items":[],"expired":true}"# 166 + ); 167 + } 168 + 169 + #[test] 170 + fn deserialize_feed() { 171 + let json = r#"{"version":"https://jsonfeed.org/version/1","title":"some title","items":[]}"#; 172 + let feed: Feed = serde_json::from_str(&json).unwrap(); 173 + let expected = Feed { 174 + version: "https://jsonfeed.org/version/1".to_string(), 175 + title: "some title".to_string(), 176 + items: vec![], 177 + ..Default::default() 178 + }; 179 + assert_eq!( 180 + feed, 181 + expected 182 + ); 183 + } 184 + 185 + #[test] 186 + fn serialize_attachment() { 187 + let attachment = Attachment { 188 + url: "http://example.com".to_string(), 189 + mime_type: "application/json".to_string(), 190 + title: Some("some title".to_string()), 191 + size_in_bytes: Some(1), 192 + duration_in_seconds: Some(1), 193 + }; 194 + assert_eq!( 195 + serde_json::to_string(&attachment).unwrap(), 196 + r#"{"url":"http://example.com","mime_type":"application/json","title":"some title","size_in_bytes":1,"duration_in_seconds":1}"# 197 + ); 198 + } 199 + 200 + #[test] 201 + fn deserialize_attachment() { 202 + let json = r#"{"url":"http://example.com","mime_type":"application/json","title":"some title","size_in_bytes":1,"duration_in_seconds":1}"#; 203 + let attachment: Attachment = serde_json::from_str(&json).unwrap(); 204 + let expected = Attachment { 205 + url: "http://example.com".to_string(), 206 + mime_type: "application/json".to_string(), 207 + title: Some("some title".to_string()), 208 + size_in_bytes: Some(1), 209 + duration_in_seconds: Some(1), 210 + }; 211 + assert_eq!( 212 + attachment, 213 + expected 214 + ); 215 + } 216 + 217 + #[test] 218 + fn serialize_author() { 219 + let author = Author { 220 + name: Some("bob jones".to_string()), 221 + url: Some("http://example.com".to_string()), 222 + avatar: Some("http://img.com/blah".to_string()), 223 + }; 224 + assert_eq!( 225 + serde_json::to_string(&author).unwrap(), 226 + r#"{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"}"# 227 + ); 228 + } 229 + 230 + #[test] 231 + fn deserialize_author() { 232 + let json = r#"{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"}"#; 233 + let author: Author = serde_json::from_str(&json).unwrap(); 234 + let expected = Author { 235 + name: Some("bob jones".to_string()), 236 + url: Some("http://example.com".to_string()), 237 + avatar: Some("http://img.com/blah".to_string()), 238 + }; 239 + assert_eq!( 240 + author, 241 + expected 242 + ); 243 + } 244 + 245 + #[test] 246 + fn serialize_hub() { 247 + let hub = Hub { 248 + type_: "some-type".to_string(), 249 + url: "http://example.com".to_string(), 250 + }; 251 + assert_eq!( 252 + serde_json::to_string(&hub).unwrap(), 253 + r#"{"type":"some-type","url":"http://example.com"}"# 254 + ) 255 + } 256 + 257 + #[test] 258 + fn deserialize_hub() { 259 + let json = r#"{"type":"some-type","url":"http://example.com"}"#; 260 + let hub: Hub = serde_json::from_str(&json).unwrap(); 261 + let expected = Hub { 262 + type_: "some-type".to_string(), 263 + url: "http://example.com".to_string(), 264 + }; 265 + assert_eq!( 266 + hub, 267 + expected 268 + ); 269 + } 270 + 271 + #[test] 272 + fn deser_podcast() { 273 + let json = r#"{ 274 + "version": "https://jsonfeed.org/version/1", 275 + "title": "Timetable", 276 + "home_page_url": "http://timetable.manton.org/", 277 + "items": [ 278 + { 279 + "id": "http://timetable.manton.org/2017/04/episode-45-launch-week/", 280 + "url": "http://timetable.manton.org/2017/04/episode-45-launch-week/", 281 + "title": "Episode 45: Launch week", 282 + "content_html": "I’m rolling out early access to Micro.blog this week. I talk about how the first 2 days have gone, mistakes with TestFlight, and what to do next.", 283 + "date_published": "2017-04-26T01:09:45+00:00", 284 + "attachments": [ 285 + { 286 + "url": "http://timetable.manton.org/podcast-download/139/episode-45-launch-week.mp3", 287 + "mime_type": "audio/mpeg", 288 + "size_in_bytes": 5236920 289 + } 290 + ] 291 + } 292 + ] 293 + }"#; 294 + serde_json::from_str::<Feed>(&json).expect("Failed to deserialize podcast feed"); 295 + } 296 + }
+493
lib/jsonfeed/src/item.rs
··· 1 + use std::fmt; 2 + use std::default::Default; 3 + 4 + use feed::{Author, Attachment}; 5 + use builder::ItemBuilder; 6 + 7 + use serde::ser::{Serialize, Serializer, SerializeStruct}; 8 + use serde::de::{self, Deserialize, Deserializer, Visitor, MapAccess}; 9 + 10 + /// Represents the `content_html` and `content_text` attributes of an item 11 + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 12 + pub enum Content { 13 + Html(String), 14 + Text(String), 15 + Both(String, String), 16 + } 17 + 18 + /// Represents an item in a feed 19 + #[derive(Debug, Clone, PartialEq)] 20 + pub struct Item { 21 + pub id: String, 22 + pub url: Option<String>, 23 + pub external_url: Option<String>, 24 + pub title: Option<String>, 25 + pub content: Content, 26 + pub summary: Option<String>, 27 + pub image: Option<String>, 28 + pub banner_image: Option<String>, 29 + pub date_published: Option<String>, // todo DateTime objects? 30 + pub date_modified: Option<String>, 31 + pub author: Option<Author>, 32 + pub tags: Option<Vec<String>>, 33 + pub attachments: Option<Vec<Attachment>>, 34 + } 35 + 36 + impl Item { 37 + pub fn builder() -> ItemBuilder { 38 + ItemBuilder::new() 39 + } 40 + } 41 + 42 + impl Default for Item { 43 + fn default() -> Item { 44 + Item { 45 + id: "".to_string(), 46 + url: None, 47 + external_url: None, 48 + title: None, 49 + content: Content::Text("".into()), 50 + summary: None, 51 + image: None, 52 + banner_image: None, 53 + date_published: None, 54 + date_modified: None, 55 + author: None, 56 + tags: None, 57 + attachments: None, 58 + } 59 + } 60 + } 61 + 62 + impl Serialize for Item { 63 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 64 + where S: Serializer 65 + { 66 + let mut state = serializer.serialize_struct("Item", 14)?; 67 + state.serialize_field("id", &self.id)?; 68 + if self.url.is_some() { 69 + state.serialize_field("url", &self.url)?; 70 + } 71 + if self.external_url.is_some() { 72 + state.serialize_field("external_url", &self.external_url)?; 73 + } 74 + if self.title.is_some() { 75 + state.serialize_field("title", &self.title)?; 76 + } 77 + match self.content { 78 + Content::Html(ref s) => { 79 + state.serialize_field("content_html", s)?; 80 + state.serialize_field("content_text", &None::<Option<&str>>)?; 81 + }, 82 + Content::Text(ref s) => { 83 + state.serialize_field("content_html", &None::<Option<&str>>)?; 84 + state.serialize_field("content_text", s)?; 85 + }, 86 + Content::Both(ref s, ref t) => { 87 + state.serialize_field("content_html", s)?; 88 + state.serialize_field("content_text", t)?; 89 + }, 90 + }; 91 + if self.summary.is_some() { 92 + state.serialize_field("summary", &self.summary)?; 93 + } 94 + if self.image.is_some() { 95 + state.serialize_field("image", &self.image)?; 96 + } 97 + if self.banner_image.is_some() { 98 + state.serialize_field("banner_image", &self.banner_image)?; 99 + } 100 + if self.date_published.is_some() { 101 + state.serialize_field("date_published", &self.date_published)?; 102 + } 103 + if self.date_modified.is_some() { 104 + state.serialize_field("date_modified", &self.date_modified)?; 105 + } 106 + if self.author.is_some() { 107 + state.serialize_field("author", &self.author)?; 108 + } 109 + if self.tags.is_some() { 110 + state.serialize_field("tags", &self.tags)?; 111 + } 112 + if self.attachments.is_some() { 113 + state.serialize_field("attachments", &self.attachments)?; 114 + } 115 + state.end() 116 + } 117 + } 118 + 119 + impl<'de> Deserialize<'de> for Item { 120 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 121 + where D: Deserializer<'de> 122 + { 123 + enum Field { 124 + Id, 125 + Url, 126 + ExternalUrl, 127 + Title, 128 + ContentHtml, 129 + ContentText, 130 + Summary, 131 + Image, 132 + BannerImage, 133 + DatePublished, 134 + DateModified, 135 + Author, 136 + Tags, 137 + Attachments, 138 + }; 139 + 140 + impl<'de> Deserialize<'de> for Field { 141 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 142 + where D: Deserializer<'de> 143 + { 144 + struct FieldVisitor; 145 + 146 + impl<'de> Visitor<'de> for FieldVisitor { 147 + type Value = Field; 148 + 149 + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 150 + formatter.write_str("non-expected field") 151 + } 152 + 153 + fn visit_str<E>(self, value: &str) -> Result<Field, E> 154 + where E: de::Error 155 + { 156 + match value { 157 + "id" => Ok(Field::Id), 158 + "url" => Ok(Field::Url), 159 + "external_url" => Ok(Field::ExternalUrl), 160 + "title" => Ok(Field::Title), 161 + "content_html" => Ok(Field::ContentHtml), 162 + "content_text" => Ok(Field::ContentText), 163 + "summary" => Ok(Field::Summary), 164 + "image" => Ok(Field::Image), 165 + "banner_image" => Ok(Field::BannerImage), 166 + "date_published" => Ok(Field::DatePublished), 167 + "date_modified" => Ok(Field::DateModified), 168 + "author" => Ok(Field::Author), 169 + "tags" => Ok(Field::Tags), 170 + "attachments" => Ok(Field::Attachments), 171 + _ => Err(de::Error::unknown_field(value, FIELDS)), 172 + } 173 + } 174 + } 175 + 176 + deserializer.deserialize_identifier(FieldVisitor) 177 + } 178 + } 179 + 180 + struct ItemVisitor; 181 + 182 + impl<'de> Visitor<'de> for ItemVisitor { 183 + type Value = Item; 184 + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 185 + formatter.write_str("non-expected thing") 186 + } 187 + 188 + fn visit_map<V>(self, mut map: V) -> Result<Item, V::Error> 189 + where V: MapAccess<'de> 190 + { 191 + let mut id = None; 192 + let mut url = None; 193 + let mut external_url = None; 194 + let mut title = None; 195 + let mut content_html: Option<String> = None; 196 + let mut content_text: Option<String> = None; 197 + let mut summary = None; 198 + let mut image = None; 199 + let mut banner_image = None; 200 + let mut date_published = None; 201 + let mut date_modified = None; 202 + let mut author = None; 203 + let mut tags = None; 204 + let mut attachments = None; 205 + 206 + while let Some(key) = map.next_key()? { 207 + match key { 208 + Field::Id => { 209 + if id.is_some() { 210 + return Err(de::Error::duplicate_field("id")); 211 + } 212 + id = Some(map.next_value()?); 213 + }, 214 + Field::Url => { 215 + if url.is_some() { 216 + return Err(de::Error::duplicate_field("url")); 217 + } 218 + url = map.next_value()?; 219 + }, 220 + Field::ExternalUrl => { 221 + if external_url.is_some() { 222 + return Err(de::Error::duplicate_field("external_url")); 223 + } 224 + external_url = map.next_value()?; 225 + }, 226 + Field::Title => { 227 + if title.is_some() { 228 + return Err(de::Error::duplicate_field("title")); 229 + } 230 + title = map.next_value()?; 231 + }, 232 + Field::ContentHtml => { 233 + if content_html.is_some() { 234 + return Err(de::Error::duplicate_field("content_html")); 235 + } 236 + content_html = map.next_value()?; 237 + }, 238 + Field::ContentText => { 239 + if content_text.is_some() { 240 + return Err(de::Error::duplicate_field("content_text")); 241 + } 242 + content_text = map.next_value()?; 243 + }, 244 + Field::Summary => { 245 + if summary.is_some() { 246 + return Err(de::Error::duplicate_field("summary")); 247 + } 248 + summary = map.next_value()?; 249 + }, 250 + Field::Image => { 251 + if image.is_some() { 252 + return Err(de::Error::duplicate_field("image")); 253 + } 254 + image = map.next_value()?; 255 + }, 256 + Field::BannerImage => { 257 + if banner_image.is_some() { 258 + return Err(de::Error::duplicate_field("banner_image")); 259 + } 260 + banner_image = map.next_value()?; 261 + }, 262 + Field::DatePublished => { 263 + if date_published.is_some() { 264 + return Err(de::Error::duplicate_field("date_published")); 265 + } 266 + date_published = map.next_value()?; 267 + }, 268 + Field::DateModified => { 269 + if date_modified.is_some() { 270 + return Err(de::Error::duplicate_field("date_modified")); 271 + } 272 + date_modified = map.next_value()?; 273 + }, 274 + Field::Author => { 275 + if author.is_some() { 276 + return Err(de::Error::duplicate_field("author")); 277 + } 278 + author = map.next_value()?; 279 + }, 280 + Field::Tags => { 281 + if tags.is_some() { 282 + return Err(de::Error::duplicate_field("tags")); 283 + } 284 + tags = map.next_value()?; 285 + }, 286 + Field::Attachments => { 287 + if attachments.is_some() { 288 + return Err(de::Error::duplicate_field("attachments")); 289 + } 290 + attachments = map.next_value()?; 291 + }, 292 + } 293 + } 294 + 295 + let id = id.ok_or_else(|| de::Error::missing_field("id"))?; 296 + let content = match (content_html, content_text) { 297 + (Some(s), Some(t)) => { 298 + Content::Both(s.to_string(), t.to_string()) 299 + }, 300 + (Some(s), _) => { 301 + Content::Html(s.to_string()) 302 + }, 303 + (_, Some(t)) => { 304 + Content::Text(t.to_string()) 305 + }, 306 + _ => return Err(de::Error::missing_field("content_html or content_text")), 307 + }; 308 + 309 + Ok(Item { 310 + id, 311 + url, 312 + external_url, 313 + title, 314 + content, 315 + summary, 316 + image, 317 + banner_image, 318 + date_published, 319 + date_modified, 320 + author, 321 + tags, 322 + attachments, 323 + }) 324 + } 325 + } 326 + 327 + const FIELDS: &'static [&'static str] = &[ 328 + "id", 329 + "url", 330 + "external_url", 331 + "title", 332 + "content", 333 + "summary", 334 + "image", 335 + "banner_image", 336 + "date_published", 337 + "date_modified", 338 + "author", 339 + "tags", 340 + "attachments", 341 + ]; 342 + deserializer.deserialize_struct("Item", FIELDS, ItemVisitor) 343 + } 344 + } 345 + 346 + #[cfg(test)] 347 + mod tests { 348 + use super::*; 349 + use feed::Author; 350 + use serde_json; 351 + 352 + #[test] 353 + #[allow(non_snake_case)] 354 + fn serialize_item__content_html() { 355 + let item = Item { 356 + id: "1".into(), 357 + url: Some("http://example.com/feed.json".into()), 358 + external_url: Some("http://example.com/feed.json".into()), 359 + title: Some("feed title".into()), 360 + content: Content::Html("<p>content</p>".into()), 361 + summary: Some("feed summary".into()), 362 + image: Some("http://img.com/blah".into()), 363 + banner_image: Some("http://img.com/blah".into()), 364 + date_published: Some("2017-01-01 10:00:00".into()), 365 + date_modified: Some("2017-01-01 10:00:00".into()), 366 + author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")), 367 + tags: Some(vec!["json".into(), "feed".into()]), 368 + attachments: Some(vec![]), 369 + }; 370 + assert_eq!( 371 + serde_json::to_string(&item).unwrap(), 372 + r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"<p>content</p>","content_text":null,"summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"# 373 + ); 374 + } 375 + 376 + #[test] 377 + #[allow(non_snake_case)] 378 + fn serialize_item__content_text() { 379 + let item = Item { 380 + id: "1".into(), 381 + url: Some("http://example.com/feed.json".into()), 382 + external_url: Some("http://example.com/feed.json".into()), 383 + title: Some("feed title".into()), 384 + content: Content::Text("content".into()), 385 + summary: Some("feed summary".into()), 386 + image: Some("http://img.com/blah".into()), 387 + banner_image: Some("http://img.com/blah".into()), 388 + date_published: Some("2017-01-01 10:00:00".into()), 389 + date_modified: Some("2017-01-01 10:00:00".into()), 390 + author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")), 391 + tags: Some(vec!["json".into(), "feed".into()]), 392 + attachments: Some(vec![]), 393 + }; 394 + assert_eq!( 395 + serde_json::to_string(&item).unwrap(), 396 + r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":null,"content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"# 397 + ); 398 + } 399 + 400 + #[test] 401 + #[allow(non_snake_case)] 402 + fn serialize_item__content_both() { 403 + let item = Item { 404 + id: "1".into(), 405 + url: Some("http://example.com/feed.json".into()), 406 + external_url: Some("http://example.com/feed.json".into()), 407 + title: Some("feed title".into()), 408 + content: Content::Both("<p>content</p>".into(), "content".into()), 409 + summary: Some("feed summary".into()), 410 + image: Some("http://img.com/blah".into()), 411 + banner_image: Some("http://img.com/blah".into()), 412 + date_published: Some("2017-01-01 10:00:00".into()), 413 + date_modified: Some("2017-01-01 10:00:00".into()), 414 + author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")), 415 + tags: Some(vec!["json".into(), "feed".into()]), 416 + attachments: Some(vec![]), 417 + }; 418 + assert_eq!( 419 + serde_json::to_string(&item).unwrap(), 420 + r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"<p>content</p>","content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"# 421 + ); 422 + } 423 + 424 + #[test] 425 + #[allow(non_snake_case)] 426 + fn deserialize_item__content_html() { 427 + let json = r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"<p>content</p>","content_text":null,"summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#; 428 + let item: Item = serde_json::from_str(&json).unwrap(); 429 + let expected = Item { 430 + id: "1".into(), 431 + url: Some("http://example.com/feed.json".into()), 432 + external_url: Some("http://example.com/feed.json".into()), 433 + title: Some("feed title".into()), 434 + content: Content::Html("<p>content</p>".into()), 435 + summary: Some("feed summary".into()), 436 + image: Some("http://img.com/blah".into()), 437 + banner_image: Some("http://img.com/blah".into()), 438 + date_published: Some("2017-01-01 10:00:00".into()), 439 + date_modified: Some("2017-01-01 10:00:00".into()), 440 + author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")), 441 + tags: Some(vec!["json".into(), "feed".into()]), 442 + attachments: Some(vec![]), 443 + }; 444 + assert_eq!(item, expected); 445 + } 446 + 447 + #[test] 448 + #[allow(non_snake_case)] 449 + fn deserialize_item__content_text() { 450 + let json = r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":null,"content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#; 451 + let item: Item = serde_json::from_str(&json).unwrap(); 452 + let expected = Item { 453 + id: "1".into(), 454 + url: Some("http://example.com/feed.json".into()), 455 + external_url: Some("http://example.com/feed.json".into()), 456 + title: Some("feed title".into()), 457 + content: Content::Text("content".into()), 458 + summary: Some("feed summary".into()), 459 + image: Some("http://img.com/blah".into()), 460 + banner_image: Some("http://img.com/blah".into()), 461 + date_published: Some("2017-01-01 10:00:00".into()), 462 + date_modified: Some("2017-01-01 10:00:00".into()), 463 + author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")), 464 + tags: Some(vec!["json".into(), "feed".into()]), 465 + attachments: Some(vec![]), 466 + }; 467 + assert_eq!(item, expected); 468 + } 469 + 470 + #[test] 471 + #[allow(non_snake_case)] 472 + fn deserialize_item__content_both() { 473 + let json = r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"<p>content</p>","content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#; 474 + let item: Item = serde_json::from_str(&json).unwrap(); 475 + let expected = Item { 476 + id: "1".into(), 477 + url: Some("http://example.com/feed.json".into()), 478 + external_url: Some("http://example.com/feed.json".into()), 479 + title: Some("feed title".into()), 480 + content: Content::Both("<p>content</p>".into(), "content".into()), 481 + summary: Some("feed summary".into()), 482 + image: Some("http://img.com/blah".into()), 483 + banner_image: Some("http://img.com/blah".into()), 484 + date_published: Some("2017-01-01 10:00:00".into()), 485 + date_modified: Some("2017-01-01 10:00:00".into()), 486 + author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")), 487 + tags: Some(vec!["json".into(), "feed".into()]), 488 + attachments: Some(vec![]), 489 + }; 490 + assert_eq!(item, expected); 491 + } 492 + } 493 +
+252
lib/jsonfeed/src/lib.rs
··· 1 + //! JSON Feed is a syndication format similar to ATOM and RSS, using JSON 2 + //! instead of XML 3 + //! 4 + //! This crate can serialize and deserialize between JSON Feed strings 5 + //! and Rust data structures. It also allows for programmatically building 6 + //! a JSON Feed 7 + //! 8 + //! Example: 9 + //! 10 + //! ```rust 11 + //! extern crate jsonfeed; 12 + //! 13 + //! use jsonfeed::{Feed, Item}; 14 + //! 15 + //! fn run() -> Result<(), jsonfeed::Error> { 16 + //! let j = r#"{ 17 + //! "title": "my feed", 18 + //! "version": "https://jsonfeed.org/version/1", 19 + //! "items": [] 20 + //! }"#; 21 + //! let feed = jsonfeed::from_str(j).unwrap(); 22 + //! 23 + //! let new_feed = Feed::builder() 24 + //! .title("some other feed") 25 + //! .item(Item::builder() 26 + //! .title("some item title") 27 + //! .content_html("<p>Hello, World</p>") 28 + //! .build()?) 29 + //! .item(Item::builder() 30 + //! .title("some other item title") 31 + //! .content_text("Hello, World!") 32 + //! .build()?) 33 + //! .build(); 34 + //! println!("{}", jsonfeed::to_string(&new_feed).unwrap()); 35 + //! Ok(()) 36 + //! } 37 + //! fn main() { 38 + //! let _ = run(); 39 + //! } 40 + //! ``` 41 + 42 + extern crate serde; 43 + #[macro_use] extern crate error_chain; 44 + #[macro_use] extern crate serde_derive; 45 + extern crate serde_json; 46 + 47 + mod errors; 48 + mod item; 49 + mod feed; 50 + mod builder; 51 + 52 + pub use errors::*; 53 + pub use item::*; 54 + pub use feed::{Feed, Author, Attachment}; 55 + 56 + use std::io::Write; 57 + 58 + /// Attempts to convert a string slice to a Feed object 59 + /// 60 + /// Example 61 + /// 62 + /// ```rust 63 + /// # extern crate jsonfeed; 64 + /// # use jsonfeed::Feed; 65 + /// # use std::default::Default; 66 + /// # fn main() { 67 + /// let json = r#"{"version": "https://jsonfeed.org/version/1", "title": "", "items": []}"#; 68 + /// let feed: Feed = jsonfeed::from_str(&json).unwrap(); 69 + /// 70 + /// assert_eq!(feed, Feed::default()); 71 + /// # } 72 + /// ``` 73 + pub fn from_str(s: &str) -> Result<Feed> { 74 + Ok(serde_json::from_str(s)?) 75 + } 76 + 77 + /// Deserialize a Feed object from an IO stream of JSON 78 + pub fn from_reader<R: ::std::io::Read>(r: R) -> Result<Feed> { 79 + Ok(serde_json::from_reader(r)?) 80 + } 81 + 82 + /// Deserialize a Feed object from bytes of JSON text 83 + pub fn from_slice<'a>(v: &'a [u8]) -> Result<Feed> { 84 + Ok(serde_json::from_slice(v)?) 85 + } 86 + 87 + /// Convert a serde_json::Value type to a Feed object 88 + pub fn from_value(value: serde_json::Value) -> Result<Feed> { 89 + Ok(serde_json::from_value(value)?) 90 + } 91 + 92 + /// Serialize a Feed to a JSON Feed string 93 + pub fn to_string(value: &Feed) -> Result<String> { 94 + Ok(serde_json::to_string(value)?) 95 + } 96 + 97 + /// Pretty-print a Feed to a JSON Feed string 98 + pub fn to_string_pretty(value: &Feed) -> Result<String> { 99 + Ok(serde_json::to_string_pretty(value)?) 100 + } 101 + 102 + /// Convert a Feed to a serde_json::Value 103 + pub fn to_value(value: Feed) -> Result<serde_json::Value> { 104 + Ok(serde_json::to_value(value)?) 105 + } 106 + 107 + /// Convert a Feed to a vector of bytes of JSON 108 + pub fn to_vec(value: &Feed) -> Result<Vec<u8>> { 109 + Ok(serde_json::to_vec(value)?) 110 + } 111 + 112 + /// Convert a Feed to a vector of bytes of pretty-printed JSON 113 + pub fn to_vec_pretty(value: &Feed) -> Result<Vec<u8>> { 114 + Ok(serde_json::to_vec_pretty(value)?) 115 + } 116 + 117 + /// Serialize a Feed to JSON and output to an IO stream 118 + pub fn to_writer<W>(writer: W, value: &Feed) -> Result<()> 119 + where W: Write 120 + { 121 + Ok(serde_json::to_writer(writer, value)?) 122 + } 123 + 124 + /// Serialize a Feed to pretty-printed JSON and output to an IO stream 125 + pub fn to_writer_pretty<W>(writer: W, value: &Feed) -> Result<()> 126 + where W: Write 127 + { 128 + Ok(serde_json::to_writer_pretty(writer, value)?) 129 + } 130 + 131 + #[cfg(test)] 132 + mod tests { 133 + use super::*; 134 + use std::io::Cursor; 135 + 136 + #[test] 137 + fn from_str() { 138 + let feed = r#"{"version": "https://jsonfeed.org/version/1","title":"","items":[]}"#; 139 + let expected = Feed::default(); 140 + assert_eq!( 141 + super::from_str(&feed).unwrap(), 142 + expected 143 + ); 144 + } 145 + #[test] 146 + fn from_reader() { 147 + let feed = r#"{"version": "https://jsonfeed.org/version/1","title":"","items":[]}"#; 148 + let feed = feed.as_bytes(); 149 + let feed = Cursor::new(feed); 150 + let expected = Feed::default(); 151 + assert_eq!( 152 + super::from_reader(feed).unwrap(), 153 + expected 154 + ); 155 + } 156 + #[test] 157 + fn from_slice() { 158 + let feed = r#"{"version": "https://jsonfeed.org/version/1","title":"","items":[]}"#; 159 + let feed = feed.as_bytes(); 160 + let expected = Feed::default(); 161 + assert_eq!( 162 + super::from_slice(&feed).unwrap(), 163 + expected 164 + ); 165 + } 166 + #[test] 167 + fn from_value() { 168 + let feed = r#"{"version": "https://jsonfeed.org/version/1","title":"","items":[]}"#; 169 + let feed: serde_json::Value = serde_json::from_str(&feed).unwrap(); 170 + let expected = Feed::default(); 171 + assert_eq!( 172 + super::from_value(feed).unwrap(), 173 + expected 174 + ); 175 + } 176 + #[test] 177 + fn to_string() { 178 + let feed = Feed::default(); 179 + let expected = r#"{"version":"https://jsonfeed.org/version/1","title":"","items":[]}"#; 180 + assert_eq!( 181 + super::to_string(&feed).unwrap(), 182 + expected 183 + ); 184 + } 185 + #[test] 186 + fn to_string_pretty() { 187 + let feed = Feed::default(); 188 + let expected = r#"{ 189 + "version": "https://jsonfeed.org/version/1", 190 + "title": "", 191 + "items": [] 192 + }"#; 193 + assert_eq!( 194 + super::to_string_pretty(&feed).unwrap(), 195 + expected 196 + ); 197 + } 198 + #[test] 199 + fn to_value() { 200 + let feed = r#"{"version":"https://jsonfeed.org/version/1","title":"","items":[]}"#; 201 + let expected: serde_json::Value = serde_json::from_str(&feed).unwrap(); 202 + assert_eq!( 203 + super::to_value(Feed::default()).unwrap(), 204 + expected 205 + ); 206 + } 207 + #[test] 208 + fn to_vec() { 209 + let feed = r#"{"version":"https://jsonfeed.org/version/1","title":"","items":[]}"#; 210 + let expected = feed.as_bytes(); 211 + assert_eq!( 212 + super::to_vec(&Feed::default()).unwrap(), 213 + expected 214 + ); 215 + } 216 + #[test] 217 + fn to_vec_pretty() { 218 + let feed = r#"{ 219 + "version": "https://jsonfeed.org/version/1", 220 + "title": "", 221 + "items": [] 222 + }"#; 223 + let expected = feed.as_bytes(); 224 + assert_eq!( 225 + super::to_vec_pretty(&Feed::default()).unwrap(), 226 + expected 227 + ); 228 + } 229 + #[test] 230 + fn to_writer() { 231 + let feed = r#"{"version":"https://jsonfeed.org/version/1","title":"","items":[]}"#; 232 + let feed = feed.as_bytes(); 233 + let mut writer = Cursor::new(Vec::with_capacity(feed.len())); 234 + super::to_writer(&mut writer, &Feed::default()).expect("Could not write to writer"); 235 + let result = writer.into_inner(); 236 + assert_eq!(result, feed); 237 + } 238 + #[test] 239 + fn to_writer_pretty() { 240 + let feed = r#"{ 241 + "version": "https://jsonfeed.org/version/1", 242 + "title": "", 243 + "items": [] 244 + }"#; 245 + let feed = feed.as_bytes(); 246 + let mut writer = Cursor::new(Vec::with_capacity(feed.len())); 247 + super::to_writer_pretty(&mut writer, &Feed::default()).expect("Could not write to writer"); 248 + let result = writer.into_inner(); 249 + assert_eq!(result, feed); 250 + } 251 + } 252 +
+1
lib/patreon/.gitignore
··· 1 + .env
+20
lib/patreon/Cargo.toml
··· 1 + [package] 2 + name = "patreon" 3 + version = "0.1.0" 4 + authors = ["Christine Dodrill <me@christine.website>"] 5 + edition = "2018" 6 + 7 + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 + 9 + [dependencies] 10 + chrono = { version = "0.4", features = ["serde"] } 11 + reqwest = { version = "0.10", features = ["json"] } 12 + serde_json = "1.0" 13 + serde = { version = "1", features = ["derive"] } 14 + thiserror = "1" 15 + log = "0" 16 + 17 + [dev-dependencies] 18 + tokio = { version = "0.2", features = ["macros"] } 19 + envy = "0.4" 20 + pretty_env_logger = "0"
+17
lib/patreon/examples/campaign.rs
··· 1 + use patreon::*; 2 + 3 + #[tokio::main] 4 + async fn main() -> Result<()> { 5 + pretty_env_logger::init(); 6 + let creds: Credentials = envy::prefixed("PATREON_").from_env().unwrap(); 7 + let cli = Client::new(creds); 8 + 9 + let camp = cli.campaign().await?; 10 + println!("{:#?}", camp); 11 + 12 + let id = camp.data[0].id.clone(); 13 + 14 + let pledges = cli.pledges(id).await?; 15 + println!("{:#?}", pledges); 16 + Ok(()) 17 + }
+158
lib/patreon/src/lib.rs
··· 1 + #[macro_use] 2 + extern crate log; 3 + 4 + use serde::{Deserialize, Serialize}; 5 + use thiserror::Error; 6 + use chrono::prelude::*; 7 + 8 + pub type Campaigns = Vec<Object<Campaign>>; 9 + pub type Pledges = Vec<Object<Pledge>>; 10 + pub type Users = Vec<Object<User>>; 11 + 12 + #[derive(Debug, Serialize, Deserialize, Clone)] 13 + pub struct Campaign { 14 + pub summary: String, 15 + pub creation_name: String, 16 + pub display_patron_goals: bool, 17 + pub pay_per_name: String, 18 + pub one_liner: Option<String>, 19 + pub main_video_embed: Option<String>, 20 + pub main_video_url: Option<String>, 21 + pub image_small_url: String, 22 + pub image_url: String, 23 + pub thanks_video_url: Option<String>, 24 + pub thanks_embed: Option<String>, 25 + pub thanks_msg: String, 26 + pub is_charged_immediately: bool, 27 + pub is_monthly: bool, 28 + pub is_nsfw: bool, 29 + pub is_plural: bool, 30 + pub created_at: DateTime<Utc>, 31 + pub published_at: DateTime<Utc>, 32 + pub pledge_url: String, 33 + pub pledge_sum: i32, 34 + pub patron_count: u32, 35 + pub creation_count: u32, 36 + pub outstanding_payment_amount_cents: u64, 37 + } 38 + 39 + #[derive(Debug, Serialize, Deserialize, Clone)] 40 + pub struct Pledge { 41 + pub amount_cents: u32, 42 + pub created_at: String, 43 + pub declined_since: Option<String>, 44 + pub pledge_cap_cents: u32, 45 + pub patron_pays_fees: bool, 46 + pub total_historical_amount_cents: Option<u32>, 47 + pub is_paused: Option<bool>, 48 + pub has_shipping_address: Option<bool>, 49 + pub outstanding_payment_amount_cents: Option<u32>, 50 + } 51 + 52 + #[derive(Debug, Serialize, Deserialize, Clone)] 53 + pub struct User { 54 + pub first_name: String, 55 + pub last_name: String, 56 + pub full_name: String, 57 + pub vanity: Option<String>, 58 + pub about: Option<String>, 59 + pub gender: i32, 60 + pub image_url: String, 61 + pub thumb_url: String, 62 + pub created: DateTime<Utc>, 63 + pub url: String, 64 + } 65 + 66 + pub type Result<T> = std::result::Result<T, Error>; 67 + 68 + #[derive(Error, Debug)] 69 + pub enum Error { 70 + #[error("json error: {0:?}")] 71 + Json(#[from] serde_json::Error), 72 + #[error("request error: {0:?}")] 73 + Request(#[from] reqwest::Error), 74 + } 75 + 76 + #[derive(Debug, Serialize, Deserialize, Clone)] 77 + pub struct Credentials { 78 + pub client_id: String, 79 + pub client_secret: String, 80 + pub access_token: String, 81 + pub refresh_token: String, 82 + } 83 + 84 + pub struct Client { 85 + cli: reqwest::Client, 86 + base_url: String, 87 + creds: Credentials, 88 + } 89 + 90 + #[derive(Debug, Serialize, Deserialize, Clone)] 91 + pub struct Data<T, U> { 92 + pub data: T, 93 + pub included: Option<Vec<U>>, 94 + } 95 + 96 + #[derive(Debug, Serialize, Deserialize, Clone)] 97 + pub struct Object<T> { 98 + pub id: String, 99 + pub attributes: T, 100 + pub r#type: String, 101 + pub links: Option<Links>, 102 + } 103 + 104 + #[derive(Debug, Serialize, Deserialize, Clone)] 105 + pub struct Links { 106 + related: String, 107 + } 108 + 109 + impl Client { 110 + pub fn new(creds: Credentials) -> Self { 111 + Self { 112 + cli: reqwest::Client::new(), 113 + base_url: "https://api.patreon.com".into(), 114 + creds: creds, 115 + } 116 + } 117 + 118 + pub async fn campaign(&self) -> Result<Data<Vec<Object<Campaign>>, ()>> { 119 + let data = self 120 + .cli 121 + .get(&format!( 122 + "{}/oauth2/api/current_user/campaigns", 123 + self.base_url 124 + )) 125 + .query(&[("include", "patron.null"), ("includes", "")]) 126 + .header( 127 + "Authorization", 128 + format!("Bearer {}", self.creds.access_token), 129 + ) 130 + .send() 131 + .await? 132 + .error_for_status()?.text().await?; 133 + log::debug!("campaign response: {}", data); 134 + Ok(serde_json::from_str(&data)?) 135 + } 136 + 137 + pub async fn pledges(&self, camp_id: String) -> Result<Vec<Object<User>>> { 138 + let data = self 139 + .cli 140 + .get(&format!( 141 + "{}/oauth2/api/campaigns/{}/pledges", 142 + self.base_url, camp_id 143 + )) 144 + .query(&[("include", "patron.null")]) 145 + .header( 146 + "Authorization", 147 + format!("Bearer {}", self.creds.access_token), 148 + ) 149 + .send() 150 + .await? 151 + .error_for_status()? 152 + .text() 153 + .await?; 154 + log::debug!("pledges for {}: {}", camp_id, data); 155 + let data : Data<Vec<Object<Pledge>>, Object<User>> = serde_json::from_str(&data)?; 156 + Ok(data.included.unwrap()) 157 + } 158 + }
-588
nix/deps.nix
··· 1 - # file generated from go.mod using vgo2nix (https://github.com/adisbladis/vgo2nix) 2 - [ 3 - { 4 - goPackagePath = "cloud.google.com/go"; 5 - fetch = { 6 - type = "git"; 7 - url = "https://code.googlesource.com/gocloud"; 8 - rev = "v0.34.0"; 9 - sha256 = "1kclgclwar3r37zbvb9gg3qxbgzkb50zk3s9778zlh2773qikmai"; 10 - }; 11 - } 12 - { 13 - goPackagePath = "github.com/alecthomas/template"; 14 - fetch = { 15 - type = "git"; 16 - url = "https://github.com/alecthomas/template"; 17 - rev = "fb15b899a751"; 18 - sha256 = "1vlasv4dgycydh5wx6jdcvz40zdv90zz1h7836z7lhsi2ymvii26"; 19 - }; 20 - } 21 - { 22 - goPackagePath = "github.com/alecthomas/units"; 23 - fetch = { 24 - type = "git"; 25 - url = "https://github.com/alecthomas/units"; 26 - rev = "c3de453c63f4"; 27 - sha256 = "0js37zlgv37y61j4a2d46jh72xm5kxmpaiw0ya9v944bjpc386my"; 28 - }; 29 - } 30 - { 31 - goPackagePath = "github.com/beorn7/perks"; 32 - fetch = { 33 - type = "git"; 34 - url = "https://github.com/beorn7/perks"; 35 - rev = "v1.0.1"; 36 - sha256 = "17n4yygjxa6p499dj3yaqzfww2g7528165cl13haj97hlx94dgl7"; 37 - }; 38 - } 39 - { 40 - goPackagePath = "github.com/celrenheit/sandflake"; 41 - fetch = { 42 - type = "git"; 43 - url = "https://github.com/celrenheit/sandflake"; 44 - rev = "50a943690bc2"; 45 - sha256 = "0ji76y79xqlx60bfxcik8zy5ha4gzhhi9qw020dkwqhbnbpaj6w2"; 46 - }; 47 - } 48 - { 49 - goPackagePath = "github.com/cespare/xxhash"; 50 - fetch = { 51 - type = "git"; 52 - url = "https://github.com/cespare/xxhash"; 53 - rev = "v2.1.1"; 54 - sha256 = "0rl5rs8546zj1vzggv38w93wx0b5dvav7yy5hzxa8kw7iikv1cgr"; 55 - }; 56 - } 57 - { 58 - goPackagePath = "github.com/davecgh/go-spew"; 59 - fetch = { 60 - type = "git"; 61 - url = "https://github.com/davecgh/go-spew"; 62 - rev = "v1.1.1"; 63 - sha256 = "0hka6hmyvp701adzag2g26cxdj47g21x6jz4sc6jjz1mn59d474y"; 64 - }; 65 - } 66 - { 67 - goPackagePath = "github.com/fsnotify/fsnotify"; 68 - fetch = { 69 - type = "git"; 70 - url = "https://github.com/fsnotify/fsnotify"; 71 - rev = "v1.4.7"; 72 - sha256 = "07va9crci0ijlivbb7q57d2rz9h27zgn2fsm60spjsqpdbvyrx4g"; 73 - }; 74 - } 75 - { 76 - goPackagePath = "github.com/go-kit/kit"; 77 - fetch = { 78 - type = "git"; 79 - url = "https://github.com/go-kit/kit"; 80 - rev = "v0.9.0"; 81 - sha256 = "09038mnw705h7isbjp8dzgp2i04bp5rqkmifxvwc5xkh75s00qpw"; 82 - }; 83 - } 84 - { 85 - goPackagePath = "github.com/go-logfmt/logfmt"; 86 - fetch = { 87 - type = "git"; 88 - url = "https://github.com/go-logfmt/logfmt"; 89 - rev = "v0.4.0"; 90 - sha256 = "06smxc112xmixz78nyvk3b2hmc7wasf2sl5vxj1xz62kqcq9lzm9"; 91 - }; 92 - } 93 - { 94 - goPackagePath = "github.com/go-stack/stack"; 95 - fetch = { 96 - type = "git"; 97 - url = "https://github.com/go-stack/stack"; 98 - rev = "v1.8.0"; 99 - sha256 = "0wk25751ryyvxclyp8jdk5c3ar0cmfr8lrjb66qbg4808x66b96v"; 100 - }; 101 - } 102 - { 103 - goPackagePath = "github.com/gogo/protobuf"; 104 - fetch = { 105 - type = "git"; 106 - url = "https://github.com/gogo/protobuf"; 107 - rev = "v1.1.1"; 108 - sha256 = "1525pq7r6h3s8dncvq8gxi893p2nq8dxpzvq0nfl5b4p6mq0v1c2"; 109 - }; 110 - } 111 - { 112 - goPackagePath = "github.com/golang/protobuf"; 113 - fetch = { 114 - type = "git"; 115 - url = "https://github.com/golang/protobuf"; 116 - rev = "v1.4.0"; 117 - sha256 = "1fjvl5n77abxz5qsd4mgyvjq19x43c5bfvmq62mq3m5plx6zksc8"; 118 - }; 119 - } 120 - { 121 - goPackagePath = "github.com/google/go-cmp"; 122 - fetch = { 123 - type = "git"; 124 - url = "https://github.com/google/go-cmp"; 125 - rev = "v0.4.0"; 126 - sha256 = "1x5pvl3fb5sbyng7i34431xycnhmx8xx94gq2n19g6p0vz68z2v2"; 127 - }; 128 - } 129 - { 130 - goPackagePath = "github.com/google/gofuzz"; 131 - fetch = { 132 - type = "git"; 133 - url = "https://github.com/google/gofuzz"; 134 - rev = "v1.0.0"; 135 - sha256 = "0qz439qvccm91w0mmjz4fqgx48clxdwagkvvx89cr43q1d4iry36"; 136 - }; 137 - } 138 - { 139 - goPackagePath = "github.com/gorilla/feeds"; 140 - fetch = { 141 - type = "git"; 142 - url = "https://github.com/gorilla/feeds"; 143 - rev = "v1.1.1"; 144 - sha256 = "1lwqibra4hyzx0jhaz12rfhfnw73bmdf8cn9r51nqidk8k7zf7sg"; 145 - }; 146 - } 147 - { 148 - goPackagePath = "github.com/hpcloud/tail"; 149 - fetch = { 150 - type = "git"; 151 - url = "https://github.com/hpcloud/tail"; 152 - rev = "v1.0.0"; 153 - sha256 = "1njpzc0pi1acg5zx9y6vj9xi6ksbsc5d387rd6904hy6rh2m6kn0"; 154 - }; 155 - } 156 - { 157 - goPackagePath = "github.com/joho/godotenv"; 158 - fetch = { 159 - type = "git"; 160 - url = "https://github.com/joho/godotenv"; 161 - rev = "v1.3.0"; 162 - sha256 = "0ri8if0pc3x6jg4c3i8wr58xyfpxkwmcjk3rp8gb398a1aa3gpjm"; 163 - }; 164 - } 165 - { 166 - goPackagePath = "github.com/json-iterator/go"; 167 - fetch = { 168 - type = "git"; 169 - url = "https://github.com/json-iterator/go"; 170 - rev = "v1.1.9"; 171 - sha256 = "0pkn2maymgl9v6vmq9q1si8xr5bbl88n6981y0lx09px6qxb29qx"; 172 - }; 173 - } 174 - { 175 - goPackagePath = "github.com/julienschmidt/httprouter"; 176 - fetch = { 177 - type = "git"; 178 - url = "https://github.com/julienschmidt/httprouter"; 179 - rev = "v1.2.0"; 180 - sha256 = "1k8bylc9s4vpvf5xhqh9h246dl1snxrzzz0614zz88cdh8yzs666"; 181 - }; 182 - } 183 - { 184 - goPackagePath = "github.com/konsorten/go-windows-terminal-sequences"; 185 - fetch = { 186 - type = "git"; 187 - url = "https://github.com/konsorten/go-windows-terminal-sequences"; 188 - rev = "v1.0.1"; 189 - sha256 = "1lchgf27n276vma6iyxa0v1xds68n2g8lih5lavqnx5x6q5pw2ip"; 190 - }; 191 - } 192 - { 193 - goPackagePath = "github.com/kr/logfmt"; 194 - fetch = { 195 - type = "git"; 196 - url = "https://github.com/kr/logfmt"; 197 - rev = "b84e30acd515"; 198 - sha256 = "02ldzxgznrfdzvghfraslhgp19la1fczcbzh7wm2zdc6lmpd1qq9"; 199 - }; 200 - } 201 - { 202 - goPackagePath = "github.com/kr/pretty"; 203 - fetch = { 204 - type = "git"; 205 - url = "https://github.com/kr/pretty"; 206 - rev = "v0.1.0"; 207 - sha256 = "18m4pwg2abd0j9cn5v3k2ksk9ig4vlwxmlw9rrglanziv9l967qp"; 208 - }; 209 - } 210 - { 211 - goPackagePath = "github.com/kr/pty"; 212 - fetch = { 213 - type = "git"; 214 - url = "https://github.com/kr/pty"; 215 - rev = "v1.1.1"; 216 - sha256 = "0383f0mb9kqjvncqrfpidsf8y6ns5zlrc91c6a74xpyxjwvzl2y6"; 217 - }; 218 - } 219 - { 220 - goPackagePath = "github.com/kr/text"; 221 - fetch = { 222 - type = "git"; 223 - url = "https://github.com/kr/text"; 224 - rev = "v0.1.0"; 225 - sha256 = "1gm5bsl01apvc84bw06hasawyqm4q84vx1pm32wr9jnd7a8vjgj1"; 226 - }; 227 - } 228 - { 229 - goPackagePath = "github.com/leanovate/gopter"; 230 - fetch = { 231 - type = "git"; 232 - url = "https://github.com/leanovate/gopter"; 233 - rev = "634a59d12406"; 234 - sha256 = "0rjx9niww7qxiqch6lwq9gibvxi41nm112yg5mzl3hpi084mb94c"; 235 - }; 236 - } 237 - { 238 - goPackagePath = "github.com/matttproud/golang_protobuf_extensions"; 239 - fetch = { 240 - type = "git"; 241 - url = "https://github.com/matttproud/golang_protobuf_extensions"; 242 - rev = "v1.0.1"; 243 - sha256 = "1d0c1isd2lk9pnfq2nk0aih356j30k3h1gi2w0ixsivi5csl7jya"; 244 - }; 245 - } 246 - { 247 - goPackagePath = "github.com/modern-go/concurrent"; 248 - fetch = { 249 - type = "git"; 250 - url = "https://github.com/modern-go/concurrent"; 251 - rev = "bacd9c7ef1dd"; 252 - sha256 = "0s0fxccsyb8icjmiym5k7prcqx36hvgdwl588y0491gi18k5i4zs"; 253 - }; 254 - } 255 - { 256 - goPackagePath = "github.com/modern-go/reflect2"; 257 - fetch = { 258 - type = "git"; 259 - url = "https://github.com/modern-go/reflect2"; 260 - rev = "v1.0.1"; 261 - sha256 = "06a3sablw53n1dqqbr2f53jyksbxdmmk8axaas4yvnhyfi55k4lf"; 262 - }; 263 - } 264 - { 265 - goPackagePath = "github.com/mwitkow/go-conntrack"; 266 - fetch = { 267 - type = "git"; 268 - url = "https://github.com/mwitkow/go-conntrack"; 269 - rev = "cc309e4a2223"; 270 - sha256 = "0nbrnpk7bkmqg9mzwsxlm0y8m7s9qd9phr1q30qlx2qmdmz7c1mf"; 271 - }; 272 - } 273 - { 274 - goPackagePath = "github.com/mxpv/patreon-go"; 275 - fetch = { 276 - type = "git"; 277 - url = "https://github.com/mxpv/patreon-go"; 278 - rev = "646111f1d983"; 279 - sha256 = "0cksf3andl8z04lychay2j0l8wrpdq7j5pdb6zy5yr4990iab6aa"; 280 - }; 281 - } 282 - { 283 - goPackagePath = "github.com/onsi/ginkgo"; 284 - fetch = { 285 - type = "git"; 286 - url = "https://github.com/onsi/ginkgo"; 287 - rev = "v1.7.0"; 288 - sha256 = "14wgpdrvpc35rdz3859bz53sc1g4vpr1fysy15wy3ff9gmqs14yg"; 289 - }; 290 - } 291 - { 292 - goPackagePath = "github.com/onsi/gomega"; 293 - fetch = { 294 - type = "git"; 295 - url = "https://github.com/onsi/gomega"; 296 - rev = "v1.4.3"; 297 - sha256 = "1c8rqg5i2hz3snmq7s41yar1zjnzilb0fyiyhkg83v97afcfx79v"; 298 - }; 299 - } 300 - { 301 - goPackagePath = "github.com/philandstuff/dhall-golang"; 302 - fetch = { 303 - type = "git"; 304 - url = "https://github.com/philandstuff/dhall-golang"; 305 - rev = "v1.0.0"; 306 - sha256 = "1ir3yhjbkqgk1z1q2v6vgbrw4q1n086mi9mbxpjrn2yn09k1h8l1"; 307 - }; 308 - } 309 - { 310 - goPackagePath = "github.com/pkg/errors"; 311 - fetch = { 312 - type = "git"; 313 - url = "https://github.com/pkg/errors"; 314 - rev = "v0.8.1"; 315 - sha256 = "0g5qcb4d4fd96midz0zdk8b9kz8xkzwfa8kr1cliqbg8sxsy5vd1"; 316 - }; 317 - } 318 - { 319 - goPackagePath = "github.com/pmezard/go-difflib"; 320 - fetch = { 321 - type = "git"; 322 - url = "https://github.com/pmezard/go-difflib"; 323 - rev = "v1.0.0"; 324 - sha256 = "0c1cn55m4rypmscgf0rrb88pn58j3ysvc2d0432dp3c6fqg6cnzw"; 325 - }; 326 - } 327 - { 328 - goPackagePath = "github.com/povilasv/prommod"; 329 - fetch = { 330 - type = "git"; 331 - url = "https://github.com/povilasv/prommod"; 332 - rev = "v0.0.12"; 333 - sha256 = "1fcmlrx0hyvwxk67p01avaz3myis3jyamhfwmyx4crgyhdc6pbb7"; 334 - }; 335 - } 336 - { 337 - goPackagePath = "github.com/prometheus/client_golang"; 338 - fetch = { 339 - type = "git"; 340 - url = "https://github.com/prometheus/client_golang"; 341 - rev = "v1.6.0"; 342 - sha256 = "0wwkx69in9dy5kzd3z6rrqf5by8cwl9r7r17fswcpx9rl3g61x1l"; 343 - }; 344 - } 345 - { 346 - goPackagePath = "github.com/prometheus/client_model"; 347 - fetch = { 348 - type = "git"; 349 - url = "https://github.com/prometheus/client_model"; 350 - rev = "v0.2.0"; 351 - sha256 = "0jffnz94d6ff39fr96b5w8i8yk26pwnrfggzz8jhi8k0yihg2c9d"; 352 - }; 353 - } 354 - { 355 - goPackagePath = "github.com/prometheus/common"; 356 - fetch = { 357 - type = "git"; 358 - url = "https://github.com/prometheus/common"; 359 - rev = "v0.9.1"; 360 - sha256 = "12pyywb02p7d30ccm41mwn69qsgqnsgv1w9jlqrrln2f1svnbqch"; 361 - }; 362 - } 363 - { 364 - goPackagePath = "github.com/prometheus/procfs"; 365 - fetch = { 366 - type = "git"; 367 - url = "https://github.com/prometheus/procfs"; 368 - rev = "v0.0.11"; 369 - sha256 = "1msc8bfywsmrgr2ryqjdqwkxiz1ll08r3qgvaka2507z1wpcpj2c"; 370 - }; 371 - } 372 - { 373 - goPackagePath = "github.com/russross/blackfriday"; 374 - fetch = { 375 - type = "git"; 376 - url = "https://github.com/russross/blackfriday"; 377 - rev = "v2.0.0"; 378 - sha256 = "10xh4zak0qbdi15nik2y72c7nn0k6vsc1iawkwx5v38cwp6hzszl"; 379 - }; 380 - } 381 - { 382 - goPackagePath = "github.com/sebest/xff"; 383 - fetch = { 384 - type = "git"; 385 - url = "https://github.com/sebest/xff"; 386 - rev = "6c115e0ffa35"; 387 - sha256 = "0l11d8mc870vxzgi74cc9dqr7kgxjmbfkfi53gc30rsyx877jx4h"; 388 - }; 389 - } 390 - { 391 - goPackagePath = "github.com/shurcooL/sanitized_anchor_name"; 392 - fetch = { 393 - type = "git"; 394 - url = "https://github.com/shurcooL/sanitized_anchor_name"; 395 - rev = "v1.0.0"; 396 - sha256 = "1gv9p2nr46z80dnfjsklc6zxbgk96349sdsxjz05f3z6wb6m5l8f"; 397 - }; 398 - } 399 - { 400 - goPackagePath = "github.com/sirupsen/logrus"; 401 - fetch = { 402 - type = "git"; 403 - url = "https://github.com/sirupsen/logrus"; 404 - rev = "v1.4.2"; 405 - sha256 = "087k2lxrr9p9dh68yw71d05h5g9p5v26zbwd6j7lghinjfaw334x"; 406 - }; 407 - } 408 - { 409 - goPackagePath = "github.com/snabb/diagio"; 410 - fetch = { 411 - type = "git"; 412 - url = "https://github.com/snabb/diagio"; 413 - rev = "v1.0.0"; 414 - sha256 = "0g4swgx30gaq0a0l71qd7c1q3dq6q8xcdnwp8063lrv8vqf3xplg"; 415 - }; 416 - } 417 - { 418 - goPackagePath = "github.com/snabb/sitemap"; 419 - fetch = { 420 - type = "git"; 421 - url = "https://github.com/snabb/sitemap"; 422 - rev = "v1.0.0"; 423 - sha256 = "0mb8r4r7dqqwdi3f9brcsqp469rsn621x9h2ahc601arjiv1zk0c"; 424 - }; 425 - } 426 - { 427 - goPackagePath = "github.com/stretchr/objx"; 428 - fetch = { 429 - type = "git"; 430 - url = "https://github.com/stretchr/objx"; 431 - rev = "v0.1.1"; 432 - sha256 = "0iph0qmpyqg4kwv8jsx6a56a7hhqq8swrazv40ycxk9rzr0s8yls"; 433 - }; 434 - } 435 - { 436 - goPackagePath = "github.com/stretchr/testify"; 437 - fetch = { 438 - type = "git"; 439 - url = "https://github.com/stretchr/testify"; 440 - rev = "v1.5.1"; 441 - sha256 = "09r89m1wy4cjv2nps1ykp00qjpi0531r07q3s34hr7m6njk4srkl"; 442 - }; 443 - } 444 - { 445 - goPackagePath = "github.com/ugorji/go"; 446 - fetch = { 447 - type = "git"; 448 - url = "https://github.com/ugorji/go"; 449 - rev = "a2c9fa250719"; 450 - sha256 = "10l24bp2vj5c99lxlkzm9icja265jmpki813v3s32ibam590virx"; 451 - }; 452 - } 453 - { 454 - goPackagePath = "golang.org/x/crypto"; 455 - fetch = { 456 - type = "git"; 457 - url = "https://go.googlesource.com/crypto"; 458 - rev = "c2843e01d9a2"; 459 - sha256 = "01xgxbj5r79nmisdvpq48zfy8pzaaj90bn6ngd4nf33j9ar1dp8r"; 460 - }; 461 - } 462 - { 463 - goPackagePath = "golang.org/x/net"; 464 - fetch = { 465 - type = "git"; 466 - url = "https://go.googlesource.com/net"; 467 - rev = "d28f0bde5980"; 468 - sha256 = "18xj31h70m7xxb7gc86n9i21w6d7djbjz67zfaljm4jqskz6hxkf"; 469 - }; 470 - } 471 - { 472 - goPackagePath = "golang.org/x/oauth2"; 473 - fetch = { 474 - type = "git"; 475 - url = "https://go.googlesource.com/oauth2"; 476 - rev = "bf48bf16ab8d"; 477 - sha256 = "1sirdib60zwmh93kf9qrx51r8544k1p9rs5mk0797wibz3m4mrdg"; 478 - }; 479 - } 480 - { 481 - goPackagePath = "golang.org/x/sync"; 482 - fetch = { 483 - type = "git"; 484 - url = "https://go.googlesource.com/sync"; 485 - rev = "cd5d95a43a6e"; 486 - sha256 = "1nqkyz2y1qvqcma52ijh02s8aiqmkfb95j08f6zcjhbga3ds6hds"; 487 - }; 488 - } 489 - { 490 - goPackagePath = "golang.org/x/sys"; 491 - fetch = { 492 - type = "git"; 493 - url = "https://go.googlesource.com/sys"; 494 - rev = "1957bb5e6d1f"; 495 - sha256 = "0imqk4l9785rw7ddvywyf8zn7k3ga6f17ky8rmf8wrri7nknr03f"; 496 - }; 497 - } 498 - { 499 - goPackagePath = "golang.org/x/text"; 500 - fetch = { 501 - type = "git"; 502 - url = "https://go.googlesource.com/text"; 503 - rev = "v0.3.0"; 504 - sha256 = "0r6x6zjzhr8ksqlpiwm5gdd7s209kwk5p4lw54xjvz10cs3qlq19"; 505 - }; 506 - } 507 - { 508 - goPackagePath = "golang.org/x/xerrors"; 509 - fetch = { 510 - type = "git"; 511 - url = "https://go.googlesource.com/xerrors"; 512 - rev = "9bdfabe68543"; 513 - sha256 = "1yjfi1bk9xb81lqn85nnm13zz725wazvrx3b50hx19qmwg7a4b0c"; 514 - }; 515 - } 516 - { 517 - goPackagePath = "google.golang.org/appengine"; 518 - fetch = { 519 - type = "git"; 520 - url = "https://github.com/golang/appengine"; 521 - rev = "v1.4.0"; 522 - sha256 = "06zl7w4sxgdq2pl94wy9ncii6h0z3szl4xpqds0sv3b3wbdlhbnn"; 523 - }; 524 - } 525 - { 526 - goPackagePath = "google.golang.org/protobuf"; 527 - fetch = { 528 - type = "git"; 529 - url = "https://go.googlesource.com/protobuf"; 530 - rev = "v1.21.0"; 531 - sha256 = "12bwln8z1lf9105gdp6ip0rx741i4yfz1520gxnp8861lh9wcl63"; 532 - }; 533 - } 534 - { 535 - goPackagePath = "gopkg.in/alecthomas/kingpin.v2"; 536 - fetch = { 537 - type = "git"; 538 - url = "https://gopkg.in/alecthomas/kingpin.v2"; 539 - rev = "v2.2.6"; 540 - sha256 = "0mndnv3hdngr3bxp7yxfd47cas4prv98sqw534mx7vp38gd88n5r"; 541 - }; 542 - } 543 - { 544 - goPackagePath = "gopkg.in/check.v1"; 545 - fetch = { 546 - type = "git"; 547 - url = "https://gopkg.in/check.v1"; 548 - rev = "41f04d3bba15"; 549 - sha256 = "0vfk9czmlxmp6wndq8k17rhnjxal764mxfhrccza7nwlia760pjy"; 550 - }; 551 - } 552 - { 553 - goPackagePath = "gopkg.in/fsnotify.v1"; 554 - fetch = { 555 - type = "git"; 556 - url = "https://gopkg.in/fsnotify.v1"; 557 - rev = "v1.4.7"; 558 - sha256 = "07va9crci0ijlivbb7q57d2rz9h27zgn2fsm60spjsqpdbvyrx4g"; 559 - }; 560 - } 561 - { 562 - goPackagePath = "gopkg.in/tomb.v1"; 563 - fetch = { 564 - type = "git"; 565 - url = "https://gopkg.in/tomb.v1"; 566 - rev = "dd632973f1e7"; 567 - sha256 = "1lqmq1ag7s4b3gc3ddvr792c5xb5k6sfn0cchr3i2s7f1c231zjv"; 568 - }; 569 - } 570 - { 571 - goPackagePath = "gopkg.in/yaml.v2"; 572 - fetch = { 573 - type = "git"; 574 - url = "https://gopkg.in/yaml.v2"; 575 - rev = "v2.2.8"; 576 - sha256 = "1inf7svydzscwv9fcjd2rm61a4xjk6jkswknybmns2n58shimapw"; 577 - }; 578 - } 579 - { 580 - goPackagePath = "within.website/ln"; 581 - fetch = { 582 - type = "git"; 583 - url = "https://github.com/Xe/ln"; 584 - rev = "v0.9.0"; 585 - sha256 = "1djbjwkyqlvf5gy5jvx0z9mm3g56fg2jjmv0ghwzlvwwpx5h338l"; 586 - }; 587 - } 588 - ]
+12
nix/sources.json
··· 11 11 "url": "https://github.com/justinwoo/easy-dhall-nix/archive/735ad924fd829c9bbee0a167e0b2bbbf91e2cad5.tar.gz", 12 12 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" 13 13 }, 14 + "naersk": { 15 + "branch": "master", 16 + "description": "Build rust crates in Nix. No configuration, no code generation, no IFD. Sandbox friendly.", 17 + "homepage": "", 18 + "owner": "nmattia", 19 + "repo": "naersk", 20 + "rev": "d5a23213d561893cebdf0d251502430334673036", 21 + "sha256": "0ifvqv3vjg80hhgxr7b22i22gh2gxw0gm5iijd9r7y4qd7n2yrcp", 22 + "type": "tarball", 23 + "url": "https://github.com/nmattia/naersk/archive/d5a23213d561893cebdf0d251502430334673036.tar.gz", 24 + "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" 25 + }, 14 26 "niv": { 15 27 "branch": "master", 16 28 "description": "Easy dependency management for Nix projects",
-6
scripts/docker.sh
··· 1 - #!/bin/sh 2 - 3 - set -e 4 - 5 - docker build -t xena/site . 6 - exec docker run --rm -itp 5030:5000 -e PORT=5000 xena/site
-4
scripts/nixrun.sh
··· 1 - #!/bin/sh 2 - 3 - cd "$(dirname "$0")"/.. 4 - ./bin/site
+10
scripts/release.sh
··· 1 + #!/usr/bin/env nix-shell 2 + #! nix-shell -p doctl -p kubectl -p curl -i bash 3 + nix-env -if ./nix/dhall-yaml.nix 4 + doctl kubernetes cluster kubeconfig save kubermemes 5 + dhall-to-yaml-ng < ./site.dhall | kubectl apply -n apps -f - 6 + kubectl rollout status -n apps deployment/christinewebsite 7 + kubectl apply -f ./k8s/job.yml 8 + sleep 10 9 + kubectl delete -f ./k8s/job.yml 10 + curl -H "Authorization: $MI_TOKEN" --data "https://christine.website/blog.json" https://mi.within.website/blog/refresh
+13 -5
shell.nix
··· 9 9 with xepkgs; 10 10 mkShell { 11 11 buildInputs = [ 12 - # Go tools 13 - go 14 - goimports 15 - gopls 16 - vgo2nix 12 + # Rust 13 + cargo 14 + cargo-watch 15 + rls 16 + rustc 17 + rustfmt 18 + 19 + # system dependencies 20 + openssl 21 + pkg-config 17 22 18 23 # kubernetes deployment 19 24 dhall ··· 26 31 ispell 27 32 ]; 28 33 34 + SITE_PREFIX = "devel."; 29 35 CLACK_SET = "Ashlynn,Terry Davis,Dennis Ritchie"; 36 + RUST_LOG = "info"; 37 + GITHUB_SHA = "devel"; 30 38 }
+21 -2
site.dhall
··· 10 10 11 11 let vars 12 12 : List kubernetes.EnvVar.Type 13 - = [ kubernetes.EnvVar::{ name = "PORT", value = Some "5000" } ] 13 + = [ kubernetes.EnvVar::{ name = "PORT", value = Some "3030" } 14 + , kubernetes.EnvVar::{ name = "RUST_LOG", value = Some "info" } 15 + , kubernetes.EnvVar::{ 16 + , name = "PATREON_CLIENT_ID" 17 + , value = Some env:PATREON_CLIENT_ID as Text 18 + } 19 + , kubernetes.EnvVar::{ 20 + , name = "PATREON_CLIENT_SECRET" 21 + , value = Some env:PATREON_CLIENT_SECRET as Text 22 + } 23 + , kubernetes.EnvVar::{ 24 + , name = "PATREON_ACCESS_TOKEN" 25 + , value = Some env:PATREON_ACCESS_TOKEN as Text 26 + } 27 + , kubernetes.EnvVar::{ 28 + , name = "PATREON_REFRESH_TOKEN" 29 + , value = Some env:PATREON_REFRESH_TOKEN as Text 30 + } 31 + ] 14 32 15 33 in kms.app.make 16 34 kms.app.Config::{ 17 35 , name = "christinewebsite" 18 - , appPort = 5000 36 + , appPort = 3030 19 37 , image = image 38 + , replicas = 2 20 39 , domain = "christine.website" 21 40 , leIssuer = "prod" 22 41 , envVars = vars
+38 -15
site.nix
··· 1 - { pkgs ? import (import ./nix/sources.nix).nixpkgs { } }: 1 + { sources ? import ./nix/sources.nix, pkgs ? import sources.nixpkgs { } }: 2 2 with pkgs; 3 3 4 - assert lib.versionAtLeast go.version "1.13"; 4 + let 5 + srcNoTarget = dir: 6 + builtins.filterSource 7 + (path: type: type != "directory" || builtins.baseNameOf path != "target") 8 + dir; 9 + 10 + naersk = pkgs.callPackage sources.naersk { }; 11 + dhallpkgs = import sources.easy-dhall-nix { inherit pkgs; }; 12 + src = srcNoTarget ./.; 13 + 14 + xesite = naersk.buildPackage { 15 + inherit src; 16 + buildInputs = [ pkg-config openssl git ]; 17 + remapPathPrefix = true; 18 + }; 19 + 20 + config = stdenv.mkDerivation { 21 + pname = "xesite-config"; 22 + version = "HEAD"; 23 + buildInputs = [ dhallpkgs.dhall-simple ]; 24 + 25 + phases = "installPhase"; 26 + 27 + installPhase = '' 28 + cd ${src} 29 + dhall resolve < ${src}/config.dhall >> $out 30 + ''; 31 + }; 5 32 6 - buildGoPackage rec { 7 - name = "christinewebsite-HEAD"; 8 - version = "latest"; 9 - goPackagePath = "christine.website"; 10 - src = ./.; 11 - goDeps = ./nix/deps.nix; 12 - allowGoReference = false; 33 + in pkgs.stdenv.mkDerivation { 34 + inherit (xesite) name; 35 + inherit src; 36 + phases = "installPhase"; 13 37 14 - preBuild = '' 15 - export CGO_ENABLED=0 16 - buildFlagsArray+=(-pkgdir "$TMPDIR") 17 - ''; 38 + installPhase = '' 39 + mkdir -p $out $out/bin 18 40 19 - postInstall = '' 41 + cp -rf ${config} $out/config.dhall 20 42 cp -rf $src/blog $out/blog 21 43 cp -rf $src/css $out/css 22 44 cp -rf $src/gallery $out/gallery 23 45 cp -rf $src/signalboost.dhall $out/signalboost.dhall 24 46 cp -rf $src/static $out/static 25 47 cp -rf $src/talks $out/talks 26 - cp -rf $src/templates $out/templates 48 + 49 + cp -rf ${xesite}/bin/xesite $out/bin/xesite 27 50 ''; 28 51 }
+191
src/app.rs
··· 1 + use crate::{post::Post, signalboost::Person}; 2 + use anyhow::Result; 3 + use atom_syndication as atom; 4 + use comrak::{markdown_to_html, ComrakOptions}; 5 + use serde::Deserialize; 6 + use std::{fs, path::PathBuf}; 7 + 8 + #[derive(Clone, Deserialize)] 9 + pub struct Config { 10 + #[serde(rename = "clackSet")] 11 + clack_set: Vec<String>, 12 + signalboost: Vec<Person>, 13 + port: u16, 14 + #[serde(rename = "resumeFname")] 15 + resume_fname: PathBuf, 16 + } 17 + 18 + pub fn markdown(inp: &str) -> String { 19 + let mut options = ComrakOptions::default(); 20 + 21 + options.extension.autolink = true; 22 + options.extension.table = true; 23 + options.extension.description_lists = true; 24 + options.extension.superscript = true; 25 + options.extension.strikethrough = true; 26 + options.extension.footnotes = true; 27 + 28 + options.render.unsafe_ = true; 29 + 30 + markdown_to_html(inp, &options) 31 + } 32 + 33 + async fn patrons() -> Result<Option<patreon::Users>> { 34 + use patreon::*; 35 + let creds: Credentials = envy::prefixed("PATREON_").from_env().unwrap(); 36 + let cli = Client::new(creds); 37 + 38 + match cli.campaign().await { 39 + Ok(camp) => { 40 + let id = camp.data[0].id.clone(); 41 + 42 + match cli.pledges(id).await { 43 + Ok(users) => Ok(Some(users)), 44 + Err(why) => { 45 + log::error!("error getting pledges: {:?}", why); 46 + Ok(None) 47 + } 48 + } 49 + } 50 + Err(why) => { 51 + log::error!("error getting patreon campaign: {:?}", why); 52 + Ok(None) 53 + } 54 + } 55 + } 56 + 57 + pub const ICON: &'static str = "https://christine.website/static/img/avatar.png"; 58 + 59 + pub struct State { 60 + pub cfg: Config, 61 + pub signalboost: Vec<Person>, 62 + pub resume: String, 63 + pub blog: Vec<Post>, 64 + pub gallery: Vec<Post>, 65 + pub talks: Vec<Post>, 66 + pub everything: Vec<Post>, 67 + pub jf: jsonfeed::Feed, 68 + pub rf: rss::Channel, 69 + pub af: atom::Feed, 70 + pub sitemap: Vec<u8>, 71 + pub patrons: Option<patreon::Users>, 72 + } 73 + 74 + pub async fn init(cfg: PathBuf) -> Result<State> { 75 + let cfg: Config = serde_dhall::from_file(cfg).parse()?; 76 + let sb = cfg.signalboost.clone(); 77 + let resume = fs::read_to_string(cfg.resume_fname.clone())?; 78 + let resume: String = markdown(&resume); 79 + let blog = crate::post::load("blog")?; 80 + let gallery = crate::post::load("gallery")?; 81 + let talks = crate::post::load("talks")?; 82 + let mut everything: Vec<Post> = vec![]; 83 + 84 + { 85 + let blog = blog.clone(); 86 + let gallery = gallery.clone(); 87 + let talks = talks.clone(); 88 + everything.extend(blog.iter().cloned()); 89 + everything.extend(gallery.iter().cloned()); 90 + everything.extend(talks.iter().cloned()); 91 + }; 92 + 93 + everything.sort(); 94 + everything.reverse(); 95 + 96 + let mut ri: Vec<rss::Item> = vec![]; 97 + let mut ai: Vec<atom::Entry> = vec![]; 98 + 99 + let mut jfb = jsonfeed::Feed::builder() 100 + .title("Christine Dodrill's Blog") 101 + .description("My blog posts and rants about various technology things.") 102 + .author( 103 + jsonfeed::Author::new() 104 + .name("Christine Dodrill") 105 + .url("https://christine.website") 106 + .avatar(ICON), 107 + ) 108 + .feed_url("https://christine.website/blog.json") 109 + .user_comment("This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1") 110 + .home_page_url("https://christine.website") 111 + .icon(ICON) 112 + .favicon(ICON); 113 + 114 + for post in &everything { 115 + let post = post.clone(); 116 + jfb = jfb.item(post.clone().into()); 117 + ri.push(post.clone().into()); 118 + ai.push(post.clone().into()); 119 + } 120 + 121 + let af = { 122 + let mut af = atom::FeedBuilder::default(); 123 + af.title("Christine Dodrill's Blog"); 124 + af.id("https://christine.website/blog"); 125 + af.generator({ 126 + let mut generator = atom::Generator::default(); 127 + generator.set_value(env!("CARGO_PKG_NAME")); 128 + generator.set_version(env!("CARGO_PKG_VERSION").to_string()); 129 + generator.set_uri("https://github.com/Xe/site".to_string()); 130 + 131 + generator 132 + }); 133 + af.entries(ai); 134 + 135 + af.build().unwrap() 136 + }; 137 + 138 + let rf = { 139 + let mut rf = rss::ChannelBuilder::default(); 140 + rf.title("Christine Dodrill's Blog"); 141 + rf.link("https://christine.website/blog"); 142 + rf.generator(crate::APPLICATION_NAME.to_string()); 143 + rf.items(ri); 144 + 145 + rf.build().unwrap() 146 + }; 147 + 148 + let mut sm: Vec<u8> = vec![]; 149 + let smw = sitemap::writer::SiteMapWriter::new(&mut sm); 150 + let mut urlwriter = smw.start_urlset()?; 151 + for url in &[ 152 + "https://christine.website/resume", 153 + "https://christine.website/contact", 154 + "https://christine.website/", 155 + "https://christine.website/blog", 156 + "https://christine.website/signalboost", 157 + ] { 158 + urlwriter.url(*url)?; 159 + } 160 + 161 + for post in &everything { 162 + urlwriter.url(format!("https://christine.website/{}", post.link))?; 163 + } 164 + 165 + urlwriter.end()?; 166 + 167 + Ok(State { 168 + cfg: cfg, 169 + signalboost: sb, 170 + resume: resume, 171 + blog: blog, 172 + gallery: gallery, 173 + talks: talks, 174 + everything: everything, 175 + jf: jfb.build(), 176 + af: af, 177 + rf: rf, 178 + sitemap: sm, 179 + patrons: patrons().await?, 180 + }) 181 + } 182 + 183 + #[cfg(test)] 184 + mod tests { 185 + use anyhow::Result; 186 + #[tokio::test] 187 + async fn init() -> Result<()> { 188 + super::init("./config.dhall".into()).await?; 189 + Ok(()) 190 + } 191 + }
+11
src/build.rs
··· 1 + use ructe::{Result, Ructe}; 2 + use std::process::Command; 3 + 4 + fn main() -> Result<()> { 5 + Ructe::from_env()?.compile_templates("templates")?; 6 + 7 + let output = Command::new("git").args(&["rev-parse", "HEAD"]).output().unwrap(); 8 + let git_hash = String::from_utf8(output.stdout).unwrap(); 9 + println!("cargo:rustc-env=GITHUB_SHA={}", git_hash); 10 + Ok(()) 11 + }
+77
src/handlers/blog.rs
··· 1 + use super::{PostNotFound, SeriesNotFound}; 2 + use crate::{ 3 + app::State, 4 + post::Post, 5 + templates::{self, Html, RenderRucte}, 6 + }; 7 + use lazy_static::lazy_static; 8 + use prometheus::{IntCounterVec, register_int_counter_vec, opts}; 9 + use std::sync::Arc; 10 + use warp::{http::Response, Rejection, Reply}; 11 + 12 + lazy_static! { 13 + static ref HIT_COUNTER: IntCounterVec = 14 + register_int_counter_vec!(opts!("blogpost_hits", "Number of hits to blogposts"), &["name"]) 15 + .unwrap(); 16 + } 17 + 18 + pub async fn index(state: Arc<State>) -> Result<impl Reply, Rejection> { 19 + let state = state.clone(); 20 + Response::builder().html(|o| templates::blogindex_html(o, state.blog.clone())) 21 + } 22 + 23 + pub async fn series(state: Arc<State>) -> Result<impl Reply, Rejection> { 24 + let state = state.clone(); 25 + let mut series: Vec<String> = vec![]; 26 + 27 + for post in &state.blog { 28 + if post.front_matter.series.is_some() { 29 + series.push(post.front_matter.series.as_ref().unwrap().clone()); 30 + } 31 + } 32 + 33 + series.sort(); 34 + series.dedup(); 35 + 36 + Response::builder().html(|o| templates::series_html(o, series)) 37 + } 38 + 39 + pub async fn series_view(series: String, state: Arc<State>) -> Result<impl Reply, Rejection> { 40 + let state = state.clone(); 41 + let mut posts: Vec<Post> = vec![]; 42 + 43 + for post in &state.blog { 44 + if post.front_matter.series.is_none() { 45 + continue; 46 + } 47 + if post.front_matter.series.as_ref().unwrap() != &series { 48 + continue; 49 + } 50 + posts.push(post.clone()); 51 + } 52 + 53 + if posts.len() == 0 { 54 + Err(SeriesNotFound(series).into()) 55 + } else { 56 + Response::builder().html(|o| templates::series_posts_html(o, series, &posts)) 57 + } 58 + } 59 + 60 + pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Rejection> { 61 + let mut want: Option<Post> = None; 62 + 63 + for post in &state.blog { 64 + if post.link == format!("blog/{}", name) { 65 + want = Some(post.clone()); 66 + } 67 + } 68 + 69 + match want { 70 + None => Err(PostNotFound("blog".into(), name).into()), 71 + Some(post) => { 72 + HIT_COUNTER.with_label_values(&[name.clone().as_str()]).inc(); 73 + let body = Html(post.body_html.clone()); 74 + Response::builder().html(|o| templates::blogpost_html(o, post, body)) 75 + } 76 + } 77 + }
+73
src/handlers/feeds.rs
··· 1 + use crate::app::State; 2 + use lazy_static::lazy_static; 3 + use prometheus::{opts, register_int_counter_vec, IntCounterVec}; 4 + use std::sync::Arc; 5 + use warp::{http::Response, Rejection, Reply}; 6 + 7 + lazy_static! { 8 + static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!( 9 + opts!("feed_hits", "Number of hits to various feeds"), 10 + &["kind"] 11 + ) 12 + .unwrap(); 13 + } 14 + 15 + pub async fn jsonfeed(state: Arc<State>) -> Result<impl Reply, Rejection> { 16 + HIT_COUNTER.with_label_values(&["json"]).inc(); 17 + let state = state.clone(); 18 + Ok(warp::reply::json(&state.jf)) 19 + } 20 + 21 + #[derive(Debug)] 22 + pub enum RenderError { 23 + WriteAtom(atom_syndication::Error), 24 + WriteRss(rss::Error), 25 + Build(warp::http::Error), 26 + } 27 + 28 + impl warp::reject::Reject for RenderError {} 29 + 30 + pub async fn atom(state: Arc<State>) -> Result<impl Reply, Rejection> { 31 + HIT_COUNTER.with_label_values(&["atom"]).inc(); 32 + let state = state.clone(); 33 + let mut buf = Vec::new(); 34 + state 35 + .af 36 + .write_to(&mut buf) 37 + .map_err(RenderError::WriteAtom) 38 + .map_err(warp::reject::custom)?; 39 + Response::builder() 40 + .status(200) 41 + .header("Content-Type", "application/atom+xml") 42 + .body(buf) 43 + .map_err(RenderError::Build) 44 + .map_err(warp::reject::custom) 45 + } 46 + 47 + pub async fn rss(state: Arc<State>) -> Result<impl Reply, Rejection> { 48 + HIT_COUNTER.with_label_values(&["rss"]).inc(); 49 + let state = state.clone(); 50 + let mut buf = Vec::new(); 51 + state 52 + .rf 53 + .write_to(&mut buf) 54 + .map_err(RenderError::WriteRss) 55 + .map_err(warp::reject::custom)?; 56 + Response::builder() 57 + .status(200) 58 + .header("Content-Type", "application/rss+xml") 59 + .body(buf) 60 + .map_err(RenderError::Build) 61 + .map_err(warp::reject::custom) 62 + } 63 + 64 + pub async fn sitemap(state: Arc<State>) -> Result<impl Reply, Rejection> { 65 + HIT_COUNTER.with_label_values(&["sitemap"]).inc(); 66 + let state = state.clone(); 67 + Response::builder() 68 + .status(200) 69 + .header("Content-Type", "application/xml") 70 + .body(state.sitemap.clone()) 71 + .map_err(RenderError::Build) 72 + .map_err(warp::reject::custom) 73 + }
+40
src/handlers/gallery.rs
··· 1 + use super::PostNotFound; 2 + use crate::{ 3 + app::State, 4 + post::Post, 5 + templates::{self, Html, RenderRucte}, 6 + }; 7 + use lazy_static::lazy_static; 8 + use prometheus::{IntCounterVec, register_int_counter_vec, opts}; 9 + use std::sync::Arc; 10 + use warp::{http::Response, Rejection, Reply}; 11 + 12 + lazy_static! { 13 + static ref HIT_COUNTER: IntCounterVec = 14 + register_int_counter_vec!(opts!("gallery_hits", "Number of hits to gallery images"), &["name"]) 15 + .unwrap(); 16 + } 17 + 18 + pub async fn index(state: Arc<State>) -> Result<impl Reply, Rejection> { 19 + let state = state.clone(); 20 + Response::builder().html(|o| templates::galleryindex_html(o, state.gallery.clone())) 21 + } 22 + 23 + pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Rejection> { 24 + let mut want: Option<Post> = None; 25 + 26 + for post in &state.gallery { 27 + if post.link == format!("gallery/{}", name) { 28 + want = Some(post.clone()); 29 + } 30 + } 31 + 32 + match want { 33 + None => Err(PostNotFound("gallery".into(), name).into()), 34 + Some(post) => { 35 + HIT_COUNTER.with_label_values(&[name.clone().as_str()]).inc(); 36 + let body = Html(post.body_html.clone()); 37 + Response::builder().html(|o| templates::gallerypost_html(o, post, body)) 38 + } 39 + } 40 + }
+145
src/handlers/mod.rs
··· 1 + use crate::{ 2 + app::State, 3 + templates::{self, Html, RenderRucte}, 4 + }; 5 + use lazy_static::lazy_static; 6 + use prometheus::{opts, register_int_counter_vec, IntCounterVec}; 7 + use std::{convert::Infallible, fmt, sync::Arc}; 8 + use warp::{ 9 + http::{Response, StatusCode}, 10 + Rejection, Reply, 11 + }; 12 + 13 + lazy_static! { 14 + static ref HIT_COUNTER: IntCounterVec = 15 + register_int_counter_vec!(opts!("hits", "Number of hits to various pages"), &["page"]) 16 + .unwrap(); 17 + } 18 + 19 + pub async fn index() -> Result<impl Reply, Rejection> { 20 + HIT_COUNTER.with_label_values(&["index"]).inc(); 21 + Response::builder().html(|o| templates::index_html(o)) 22 + } 23 + 24 + pub async fn contact() -> Result<impl Reply, Rejection> { 25 + HIT_COUNTER.with_label_values(&["contact"]).inc(); 26 + Response::builder().html(|o| templates::contact_html(o)) 27 + } 28 + 29 + pub async fn feeds() -> Result<impl Reply, Rejection> { 30 + HIT_COUNTER.with_label_values(&["feeds"]).inc(); 31 + Response::builder().html(|o| templates::feeds_html(o)) 32 + } 33 + 34 + pub async fn resume(state: Arc<State>) -> Result<impl Reply, Rejection> { 35 + HIT_COUNTER.with_label_values(&["resume"]).inc(); 36 + let state = state.clone(); 37 + Response::builder().html(|o| templates::resume_html(o, Html(state.resume.clone()))) 38 + } 39 + 40 + pub async fn patrons(state: Arc<State>) -> Result<impl Reply, Rejection> { 41 + HIT_COUNTER.with_label_values(&["patrons"]).inc(); 42 + let state = state.clone(); 43 + match &state.patrons { 44 + None => Response::builder().status(500).html(|o| { 45 + templates::error_html( 46 + o, 47 + "Could not load patrons, let me know the API token expired again".to_string(), 48 + ) 49 + }), 50 + Some(patrons) => Response::builder().html(|o| templates::patrons_html(o, patrons.clone())), 51 + } 52 + } 53 + 54 + pub async fn signalboost(state: Arc<State>) -> Result<impl Reply, Rejection> { 55 + HIT_COUNTER.with_label_values(&["signalboost"]).inc(); 56 + let state = state.clone(); 57 + Response::builder().html(|o| templates::signalboost_html(o, state.signalboost.clone())) 58 + } 59 + 60 + pub async fn not_found() -> Result<impl Reply, Rejection> { 61 + HIT_COUNTER.with_label_values(&["not_found"]).inc(); 62 + Response::builder().html(|o| templates::notfound_html(o, "some path".into())) 63 + } 64 + 65 + pub mod blog; 66 + pub mod feeds; 67 + pub mod gallery; 68 + pub mod talks; 69 + 70 + #[derive(Debug, thiserror::Error)] 71 + struct PostNotFound(String, String); 72 + 73 + impl fmt::Display for PostNotFound { 74 + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 75 + write!(f, "not found: {}/{}", self.0, self.1) 76 + } 77 + } 78 + 79 + impl warp::reject::Reject for PostNotFound {} 80 + 81 + impl From<PostNotFound> for warp::reject::Rejection { 82 + fn from(error: PostNotFound) -> Self { 83 + warp::reject::custom(error) 84 + } 85 + } 86 + 87 + #[derive(Debug, thiserror::Error)] 88 + struct SeriesNotFound(String); 89 + 90 + impl fmt::Display for SeriesNotFound { 91 + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 92 + write!(f, "{}", self.0) 93 + } 94 + } 95 + 96 + impl warp::reject::Reject for SeriesNotFound {} 97 + 98 + impl From<SeriesNotFound> for warp::reject::Rejection { 99 + fn from(error: SeriesNotFound) -> Self { 100 + warp::reject::custom(error) 101 + } 102 + } 103 + 104 + lazy_static! { 105 + static ref REJECTION_COUNTER: IntCounterVec = register_int_counter_vec!( 106 + opts!("rejections", "Number of rejections by kind"), 107 + &["kind"] 108 + ) 109 + .unwrap(); 110 + } 111 + 112 + pub async fn rejection(err: Rejection) -> Result<impl Reply, Infallible> { 113 + let path: String; 114 + let code; 115 + 116 + if err.is_not_found() { 117 + REJECTION_COUNTER.with_label_values(&["404"]).inc(); 118 + path = "".into(); 119 + code = StatusCode::NOT_FOUND; 120 + } else if let Some(SeriesNotFound(series)) = err.find() { 121 + REJECTION_COUNTER 122 + .with_label_values(&["SeriesNotFound"]) 123 + .inc(); 124 + log::error!("invalid series {}", series); 125 + path = format!("/blog/series/{}", series); 126 + code = StatusCode::NOT_FOUND; 127 + } else if let Some(PostNotFound(kind, name)) = err.find() { 128 + REJECTION_COUNTER.with_label_values(&["PostNotFound"]).inc(); 129 + log::error!("unknown post {}/{}", kind, name); 130 + path = format!("/{}/{}", kind, name); 131 + code = StatusCode::NOT_FOUND; 132 + } else { 133 + REJECTION_COUNTER.with_label_values(&["Other"]).inc(); 134 + log::error!("unhandled rejection: {:?}", err); 135 + path = format!("weird rejection: {:?}", err); 136 + code = StatusCode::INTERNAL_SERVER_ERROR; 137 + } 138 + 139 + Ok(warp::reply::with_status( 140 + Response::builder() 141 + .html(|o| templates::notfound_html(o, path)) 142 + .unwrap(), 143 + code, 144 + )) 145 + }
+40
src/handlers/talks.rs
··· 1 + use super::PostNotFound; 2 + use crate::{ 3 + app::State, 4 + post::Post, 5 + templates::{self, Html, RenderRucte}, 6 + }; 7 + use lazy_static::lazy_static; 8 + use prometheus::{IntCounterVec, register_int_counter_vec, opts}; 9 + use std::sync::Arc; 10 + use warp::{http::Response, Rejection, Reply}; 11 + 12 + lazy_static! { 13 + static ref HIT_COUNTER: IntCounterVec = 14 + register_int_counter_vec!(opts!("talks_hits", "Number of hits to talks images"), &["name"]) 15 + .unwrap(); 16 + } 17 + 18 + pub async fn index(state: Arc<State>) -> Result<impl Reply, Rejection> { 19 + let state = state.clone(); 20 + Response::builder().html(|o| templates::talkindex_html(o, state.talks.clone())) 21 + } 22 + 23 + pub async fn post_view(name: String, state: Arc<State>) -> Result<impl Reply, Rejection> { 24 + let mut want: Option<Post> = None; 25 + 26 + for post in &state.talks { 27 + if post.link == format!("talks/{}", name) { 28 + want = Some(post.clone()); 29 + } 30 + } 31 + 32 + match want { 33 + None => Err(PostNotFound("talks".into(), name).into()), 34 + Some(post) => { 35 + HIT_COUNTER.with_label_values(&[name.clone().as_str()]).inc(); 36 + let body = Html(post.body_html.clone()); 37 + Response::builder().html(|o| templates::talkpost_html(o, post, body)) 38 + } 39 + } 40 + }
+154
src/main.rs
··· 1 + use anyhow::Result; 2 + use hyper::{header::CONTENT_TYPE, Body, Response}; 3 + use prometheus::{Encoder, TextEncoder}; 4 + use std::sync::Arc; 5 + use warp::{path, Filter}; 6 + 7 + pub mod app; 8 + pub mod handlers; 9 + pub mod post; 10 + pub mod signalboost; 11 + 12 + use app::State; 13 + 14 + const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 15 + 16 + fn with_state( 17 + state: Arc<State>, 18 + ) -> impl Filter<Extract = (Arc<State>,), Error = std::convert::Infallible> + Clone { 19 + warp::any().map(move || state.clone()) 20 + } 21 + 22 + #[tokio::main] 23 + async fn main() -> Result<()> { 24 + let _ = kankyo::init(); 25 + pretty_env_logger::init(); 26 + log::info!("starting up commit {}", env!("GITHUB_SHA")); 27 + 28 + let state = Arc::new(app::init( 29 + std::env::var("CONFIG_FNAME") 30 + .unwrap_or("./config.dhall".into()) 31 + .as_str() 32 + .into(), 33 + ).await?); 34 + 35 + let healthcheck = warp::get().and(warp::path(".within").and(warp::path("health")).map(|| "OK")); 36 + 37 + let base = warp::path!("blog" / ..); 38 + let blog_index = base 39 + .and(warp::path::end()) 40 + .and(with_state(state.clone())) 41 + .and_then(handlers::blog::index); 42 + let series = base 43 + .and(warp::path!("series").and(with_state(state.clone()).and_then(handlers::blog::series))); 44 + let series_view = base.and( 45 + warp::path!("series" / String) 46 + .and(with_state(state.clone())) 47 + .and(warp::get()) 48 + .and_then(handlers::blog::series_view), 49 + ); 50 + let post_view = base.and( 51 + warp::path!(String) 52 + .and(with_state(state.clone())) 53 + .and(warp::get()) 54 + .and_then(handlers::blog::post_view), 55 + ); 56 + 57 + let gallery_base = warp::path!("gallery" / ..); 58 + let gallery_index = gallery_base 59 + .and(warp::path::end()) 60 + .and(with_state(state.clone())) 61 + .and_then(handlers::gallery::index); 62 + let gallery_post_view = gallery_base.and( 63 + warp::path!(String) 64 + .and(with_state(state.clone())) 65 + .and(warp::get()) 66 + .and_then(handlers::gallery::post_view), 67 + ); 68 + 69 + let talk_base = warp::path!("talks" / ..); 70 + let talk_index = talk_base 71 + .and(warp::path::end()) 72 + .and(with_state(state.clone())) 73 + .and_then(handlers::talks::index); 74 + let talk_post_view = talk_base.and( 75 + warp::path!(String) 76 + .and(with_state(state.clone())) 77 + .and(warp::get()) 78 + .and_then(handlers::talks::post_view), 79 + ); 80 + 81 + let index = warp::get().and(path::end().and_then(handlers::index)); 82 + let contact = warp::path!("contact").and_then(handlers::contact); 83 + let feeds = warp::path!("feeds").and_then(handlers::feeds); 84 + let resume = warp::path!("resume") 85 + .and(with_state(state.clone())) 86 + .and_then(handlers::resume); 87 + let signalboost = warp::path!("signalboost") 88 + .and(with_state(state.clone())) 89 + .and_then(handlers::signalboost); 90 + let patrons = warp::path!("patrons") 91 + .and(with_state(state.clone())) 92 + .and_then(handlers::patrons); 93 + 94 + let files = warp::path("static").and(warp::fs::dir("./static")); 95 + let css = warp::path("css").and(warp::fs::dir("./css")); 96 + let sw = warp::path("sw.js").and(warp::fs::file("./static/js/sw.js")); 97 + let robots = warp::path("robots.txt").and(warp::fs::file("./static/robots.txt")); 98 + let favicon = warp::path("favicon.ico").and(warp::fs::file("./static/favicon/favicon.ico")); 99 + 100 + let jsonfeed = warp::path("blog.json") 101 + .and(with_state(state.clone())) 102 + .and_then(handlers::feeds::jsonfeed); 103 + let atom = warp::path("blog.atom") 104 + .and(with_state(state.clone())) 105 + .and_then(handlers::feeds::atom); 106 + let rss = warp::path("blog.rss") 107 + .and(with_state(state.clone())) 108 + .and_then(handlers::feeds::rss); 109 + let sitemap = warp::path("sitemap.xml") 110 + .and(with_state(state.clone())) 111 + .and_then(handlers::feeds::sitemap); 112 + 113 + let go_vanity_jsonfeed = warp::path("jsonfeed") 114 + .and(warp::any().map(move || "christine.website/jsonfeed")) 115 + .and(warp::any().map(move || "https://tulpa.dev/Xe/jsonfeed")) 116 + .and_then(go_vanity::gitea); 117 + 118 + let metrics_endpoint = warp::path("metrics").and(warp::path::end()).map(move || { 119 + let encoder = TextEncoder::new(); 120 + let metric_families = prometheus::gather(); 121 + let mut buffer = vec![]; 122 + encoder.encode(&metric_families, &mut buffer).unwrap(); 123 + Response::builder() 124 + .status(200) 125 + .header(CONTENT_TYPE, encoder.format_type()) 126 + .body(Body::from(buffer)) 127 + .unwrap() 128 + }); 129 + 130 + let site = index 131 + .or(contact.or(feeds).or(resume.or(signalboost)).or(patrons)) 132 + .or(blog_index.or(series.or(series_view).or(post_view))) 133 + .or(gallery_index.or(gallery_post_view)) 134 + .or(talk_index.or(talk_post_view)) 135 + .or(jsonfeed.or(atom).or(rss.or(sitemap))) 136 + .or(files.or(css).or(favicon).or(sw.or(robots))) 137 + .or(healthcheck.or(metrics_endpoint).or(go_vanity_jsonfeed)) 138 + .map(|reply| { 139 + warp::reply::with_header( 140 + reply, 141 + "X-Hacker", 142 + "If you are reading this, check out /signalboost to find people for your team", 143 + ) 144 + }) 145 + .map(|reply| warp::reply::with_header(reply, "X-Clacks-Overhead", "GNU Ashlynn")) 146 + .with(warp::log(APPLICATION_NAME)) 147 + .recover(handlers::rejection); 148 + 149 + warp::serve(site).run(([0, 0, 0, 0], 3030)).await; 150 + 151 + Ok(()) 152 + } 153 + 154 + include!(concat!(env!("OUT_DIR"), "/templates.rs"));
+114
src/post/frontmatter.rs
··· 1 + /// This code was borrowed from @fasterthanlime. 2 + 3 + use anyhow::{Result}; 4 + use serde::{Serialize, Deserialize}; 5 + 6 + #[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] 7 + pub struct Data { 8 + pub title: String, 9 + pub date: String, 10 + pub series: Option<String>, 11 + pub tags: Option<Vec<String>>, 12 + pub slides_link: Option<String>, 13 + pub image: Option<String>, 14 + pub thumb: Option<String>, 15 + pub show: Option<bool>, 16 + } 17 + 18 + enum State { 19 + SearchForStart, 20 + ReadingMarker { count: usize, end: bool }, 21 + ReadingFrontMatter { buf: String, line_start: bool }, 22 + SkipNewline { end: bool }, 23 + } 24 + 25 + #[derive(Debug, thiserror::Error)] 26 + enum Error { 27 + #[error("EOF while parsing frontmatter")] 28 + EOF, 29 + #[error("Error parsing yaml: {0:?}")] 30 + Yaml(#[from] serde_yaml::Error), 31 + } 32 + 33 + impl Data { 34 + pub fn parse(input: &str) -> Result<(Data, usize)> { 35 + let mut state = State::SearchForStart; 36 + 37 + let mut payload = None; 38 + let offset; 39 + 40 + let mut chars = input.char_indices(); 41 + 'parse: loop { 42 + let (idx, ch) = match chars.next() { 43 + Some(x) => x, 44 + None => return Err(Error::EOF)?, 45 + }; 46 + match &mut state { 47 + State::SearchForStart => match ch { 48 + '-' => { 49 + state = State::ReadingMarker { 50 + count: 1, 51 + end: false, 52 + }; 53 + } 54 + '\n' | '\t' | ' ' => { 55 + // ignore whitespace 56 + } 57 + _ => { 58 + panic!("Start of frontmatter not found"); 59 + } 60 + }, 61 + State::ReadingMarker { count, end } => match ch { 62 + '-' => { 63 + *count += 1; 64 + if *count == 3 { 65 + state = State::SkipNewline { end: *end }; 66 + } 67 + } 68 + _ => { 69 + panic!("Malformed frontmatter marker"); 70 + } 71 + }, 72 + State::SkipNewline { end } => match ch { 73 + '\n' => { 74 + if *end { 75 + offset = idx + 1; 76 + break 'parse; 77 + } else { 78 + state = State::ReadingFrontMatter { 79 + buf: String::new(), 80 + line_start: true, 81 + }; 82 + } 83 + } 84 + _ => panic!("Expected newline, got {:?}",), 85 + }, 86 + State::ReadingFrontMatter { buf, line_start } => match ch { 87 + '-' if *line_start => { 88 + let mut state_temp = State::ReadingMarker { 89 + count: 1, 90 + end: true, 91 + }; 92 + std::mem::swap(&mut state, &mut state_temp); 93 + if let State::ReadingFrontMatter { buf, .. } = state_temp { 94 + payload = Some(buf); 95 + } else { 96 + unreachable!(); 97 + } 98 + } 99 + ch => { 100 + buf.push(ch); 101 + *line_start = ch == '\n'; 102 + } 103 + }, 104 + } 105 + } 106 + 107 + // unwrap justification: option set in state machine, Rust can't statically analyze it 108 + let payload = payload.unwrap(); 109 + 110 + let fm: Self = serde_yaml::from_str(&payload)?; 111 + 112 + Ok((fm, offset)) 113 + } 114 + }
+178
src/post/mod.rs
··· 1 + use anyhow::{anyhow, Result}; 2 + use atom_syndication as atom; 3 + use chrono::prelude::*; 4 + use glob::glob; 5 + use std::{cmp::Ordering, fs}; 6 + 7 + pub mod frontmatter; 8 + 9 + #[derive(Eq, PartialEq, Debug, Clone)] 10 + pub struct Post { 11 + pub front_matter: frontmatter::Data, 12 + pub link: String, 13 + pub body: String, 14 + pub body_html: String, 15 + pub date: DateTime<FixedOffset>, 16 + } 17 + 18 + impl Into<jsonfeed::Item> for Post { 19 + fn into(self) -> jsonfeed::Item { 20 + let mut result = jsonfeed::Item::builder() 21 + .title(self.front_matter.title) 22 + .content_html(self.body_html) 23 + .content_text(self.body) 24 + .id(format!("https://christine.website/{}", self.link)) 25 + .url(format!("https://christine.website/{}", self.link)) 26 + .date_published(self.date.to_rfc3339()) 27 + .author( 28 + jsonfeed::Author::new() 29 + .name("Christine Dodrill") 30 + .url("https://christine.website") 31 + .avatar("https://christine.website/static/img/avatar.png"), 32 + ); 33 + 34 + let mut tags: Vec<String> = vec![]; 35 + 36 + if let Some(series) = self.front_matter.series { 37 + tags.push(series); 38 + } 39 + 40 + if let Some(mut meta_tags) = self.front_matter.tags { 41 + tags.append(&mut meta_tags); 42 + } 43 + 44 + if tags.len() != 0 { 45 + result = result.tags(tags); 46 + } 47 + 48 + if let Some(image_url) = self.front_matter.image { 49 + result = result.image(image_url); 50 + } 51 + 52 + result.build().unwrap() 53 + } 54 + } 55 + 56 + impl Into<atom::Entry> for Post { 57 + fn into(self) -> atom::Entry { 58 + let mut content = atom::ContentBuilder::default(); 59 + 60 + content.src(format!("https://christine.website/{}", self.link)); 61 + content.content_type(Some("text/html;charset=utf-8".into())); 62 + content.value(Some(xml::escape::escape_str_pcdata(&self.body_html).into())); 63 + 64 + let content = content.build().unwrap(); 65 + 66 + let mut result = atom::EntryBuilder::default(); 67 + result.id(format!("https://christine.website/{}", self.link)); 68 + result.contributors({ 69 + let mut me = atom::Person::default(); 70 + 71 + me.set_name("Christine Dodrill"); 72 + me.set_email("me@christine.website".to_string()); 73 + me.set_uri("https://christine.website".to_string()); 74 + 75 + vec![me] 76 + }); 77 + result.title(self.front_matter.title); 78 + let mut link = atom::Link::default(); 79 + link.href = format!("https://christine.website/{}", self.link); 80 + result.links(vec![link]); 81 + result.content(content); 82 + result.published(self.date); 83 + 84 + result.build().unwrap() 85 + } 86 + } 87 + 88 + impl Into<rss::Item> for Post { 89 + fn into(self) -> rss::Item { 90 + let mut guid = rss::Guid::default(); 91 + guid.set_value(format!("https://christine.website/{}", self.link)); 92 + let mut result = rss::ItemBuilder::default(); 93 + result.title(Some(self.front_matter.title)); 94 + result.link(format!("https://christine.website/{}", self.link)); 95 + result.guid(guid); 96 + result.author(Some("me@christine.website (Christine Dodrill)".to_string())); 97 + result.content(self.body_html); 98 + result.pub_date(self.date.to_rfc2822()); 99 + 100 + result.build().unwrap() 101 + } 102 + } 103 + 104 + impl Ord for Post { 105 + fn cmp(&self, other: &Self) -> Ordering { 106 + self.partial_cmp(&other).unwrap() 107 + } 108 + } 109 + 110 + impl PartialOrd for Post { 111 + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 112 + Some(self.date.cmp(&other.date)) 113 + } 114 + } 115 + 116 + impl Post { 117 + pub fn detri(&self) -> String { 118 + self.date.format("M%m %d %Y").to_string() 119 + } 120 + } 121 + 122 + pub fn load(dir: &str) -> Result<Vec<Post>> { 123 + let mut result: Vec<Post> = vec![]; 124 + 125 + for path in glob(&format!("{}/*.markdown", dir))?.filter_map(Result::ok) { 126 + let body = fs::read_to_string(path.clone())?; 127 + let (fm, content_offset) = frontmatter::Data::parse(body.clone().as_str())?; 128 + let markup = &body[content_offset..]; 129 + let date = NaiveDate::parse_from_str(&fm.clone().date, "%Y-%m-%d")?; 130 + 131 + result.push(Post { 132 + front_matter: fm, 133 + link: format!("{}/{}", dir, path.file_stem().unwrap().to_str().unwrap()), 134 + body: markup.to_string(), 135 + body_html: crate::app::markdown(&markup), 136 + date: { 137 + DateTime::<Utc>::from_utc( 138 + NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)), 139 + Utc, 140 + ) 141 + .with_timezone(&Utc) 142 + .into() 143 + }, 144 + }) 145 + } 146 + 147 + if result.len() == 0 { 148 + Err(anyhow!("no posts loaded")) 149 + } else { 150 + result.sort(); 151 + result.reverse(); 152 + Ok(result) 153 + } 154 + } 155 + 156 + #[cfg(test)] 157 + mod tests { 158 + use super::*; 159 + use anyhow::Result; 160 + 161 + #[test] 162 + fn blog() -> Result<()> { 163 + load("blog")?; 164 + Ok(()) 165 + } 166 + 167 + #[test] 168 + fn gallery() -> Result<()> { 169 + load("gallery")?; 170 + Ok(()) 171 + } 172 + 173 + #[test] 174 + fn talks() -> Result<()> { 175 + load("talks")?; 176 + Ok(()) 177 + } 178 + }
+23
src/signalboost.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Clone, Debug, Deserialize)] 4 + pub struct Person { 5 + pub name: String, 6 + pub tags: Vec<String>, 7 + 8 + #[serde(rename = "gitLink")] 9 + pub git_link: String, 10 + 11 + pub twitter: String, 12 + } 13 + 14 + #[cfg(test)] 15 + mod tests { 16 + use anyhow::Result; 17 + #[test] 18 + fn load() -> Result<()> { 19 + let _people: Vec<super::Person> = serde_dhall::from_file("./signalboost.dhall").parse()?; 20 + 21 + Ok(()) 22 + } 23 + }
+7
static/js/installsw.js
··· 1 + if (navigator.serviceWorker.controller) { 2 + console.log("Active service worker found, no need to register"); 3 + } else { 4 + navigator.serviceWorker.register("/sw.js").then(function(reg) { 5 + console.log("Service worker has been registered for scope:" + reg.scope); 6 + }); 7 + }
-2
static/js/instantpage-3.0.0.js
··· 1 - /*! instant.page v3.0.0 - (C) 2019 Alexandre Dieulot - https://instant.page/license */ 2 - let t,e;const n=new Set,o=document.createElement("link"),s=o.relList&&o.relList.supports&&o.relList.supports("prefetch")&&window.IntersectionObserver&&"isIntersecting"in IntersectionObserverEntry.prototype,i="instantAllowQueryString"in document.body.dataset,r="instantAllowExternalLinks"in document.body.dataset,a="instantWhitelist"in document.body.dataset;let c=65,d=!1,l=!1,u=!1;if("instantIntensity"in document.body.dataset){const t=document.body.dataset.instantIntensity;if("mousedown"==t.substr(0,"mousedown".length))d=!0,"mousedown-only"==t&&(l=!0);else if("viewport"==t.substr(0,"viewport".length))navigator.connection&&(navigator.connection.saveData||navigator.connection.effectiveType.includes("2g"))||("viewport"==t?document.documentElement.clientWidth*document.documentElement.clientHeight<45e4&&(u=!0):"viewport-all"==t&&(u=!0));else{const e=parseInt(t);isNaN(e)||(c=e)}}if(s){const n={capture:!0,passive:!0};if(l||document.addEventListener("touchstart",function(t){e=performance.now();const n=t.target.closest("a");if(!f(n))return;h(n.href)},n),d?document.addEventListener("mousedown",function(t){const e=t.target.closest("a");if(!f(e))return;h(e.href)},n):document.addEventListener("mouseover",function(n){if(performance.now()-e<1100)return;const o=n.target.closest("a");if(!f(o))return;o.addEventListener("mouseout",m,{passive:!0}),t=setTimeout(()=>{h(o.href),t=void 0},c)},n),u){let t;(t=window.requestIdleCallback?t=>{requestIdleCallback(t,{timeout:1500})}:t=>{t()})(()=>{const t=new IntersectionObserver(e=>{e.forEach(e=>{if(e.isIntersecting){const n=e.target;t.unobserve(n),h(n.href)}})});document.querySelectorAll("a").forEach(e=>{f(e)&&t.observe(e)})})}}function m(e){e.relatedTarget&&e.target.closest("a")==e.relatedTarget.closest("a")||t&&(clearTimeout(t),t=void 0)}function f(t){if(t&&t.href&&(!a||"instant"in t.dataset)&&(r||t.origin==location.origin||"instant"in t.dataset)&&["http:","https:"].includes(t.protocol)&&("http:"!=t.protocol||"https:"!=location.protocol)&&(i||!t.search||"instant"in t.dataset)&&!(t.hash&&t.pathname+t.search==location.pathname+location.search||"noInstant"in t.dataset))return!0}function h(t){if(n.has(t))return;const e=document.createElement("link");e.rel="prefetch",e.href=t,document.head.appendChild(e),n.add(t)}
+23 -23
static/js/sw.js
··· 5 5 event.waitUntil(preLoad()); 6 6 }); 7 7 8 - const cacheName = "cache-2019-11-01"; 8 + const cacheName = "cache-xesite-2.0.0"; 9 9 10 10 var preLoad = function(){ 11 - console.log('[PWA Builder] Install Event processing'); 12 - return caches.open(cacheName).then(function(cache) { 13 - console.log('[PWA Builder] Cached index and offline page during Install'); 14 - return cache.addAll(['/blog/', '/blog', '/', '/contact', '/resume', '/talks', '/gallery']); 15 - }); 11 + console.log('[PWA Builder] Install Event processing'); 12 + return caches.open(cacheName).then(function(cache) { 13 + console.log('[PWA Builder] Cached index and offline page during Install'); 14 + return cache.addAll(['/blog/', '/blog', '/', '/contact', '/resume', '/talks', '/gallery', '/signalboost']); 15 + }); 16 16 }; 17 17 18 18 self.addEventListener('fetch', function(event) { 19 - if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') { 20 - return; 21 - } 22 - console.log('[PWA Builder] The service worker is serving the asset.'); 23 - event.respondWith(checkResponse(event.request).catch(function() { 24 - return returnFromCache(event.request); 25 - })); 26 - event.waitUntil(addToCache(event.request)); 19 + if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') { 20 + return; 21 + } 22 + console.log('[PWA Builder] The service worker is serving the asset.'); 23 + event.respondWith(checkResponse(event.request).catch(function() { 24 + return returnFromCache(event.request); 25 + })); 26 + event.waitUntil(addToCache(event.request)); 27 27 }); 28 28 29 29 var checkResponse = function(request){ 30 - return new Promise(function(fulfill, reject) { 31 - fetch(request).then(function(response){ 32 - if(response.status !== 404) { 33 - fulfill(response); 34 - } else { 35 - reject(); 36 - } 37 - }, reject); 38 - }); 30 + return new Promise(function(fulfill, reject) { 31 + fetch(request).then(function(response){ 32 + if(response.status !== 404) { 33 + fulfill(response); 34 + } else { 35 + reject(); 36 + } 37 + }, reject); 38 + }); 39 39 }; 40 40 41 41 var addToCache = function(request){
-91
templates/base.html
··· 1 - <!DOCTYPE html> 2 - <html lang="en"> 3 - <head> 4 - {{ template "title" . }} 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <meta name="go-import" content="christine.website git https://github.com/Xe/site"> 7 - <link rel="stylesheet" href="/css/hack.css" /> 8 - <link rel="stylesheet" href="/css/gruvbox-dark.css" /> 9 - <!-- <link rel="stylesheet" href="/css/snow.css" /> --> 10 - <link rel="manifest" href="/static/manifest.json" /> 11 - 12 - <link rel="alternate" type="application/rss+xml" href="https://christine.website/blog.rss" /> 13 - <link rel="alternate" type="application/atom+xml" href="https://christine.website/blog.atom" /> 14 - <link rel="alternate" title="My Feed" type="application/json" href="https://christine.website/blog.json" /> 15 - 16 - <link rel="apple-touch-icon" sizes="57x57" href="/static/favicon/apple-icon-57x57.png"> 17 - <link rel="apple-touch-icon" sizes="60x60" href="/static/favicon/apple-icon-60x60.png"> 18 - <link rel="apple-touch-icon" sizes="72x72" href="/static/favicon/apple-icon-72x72.png"> 19 - <link rel="apple-touch-icon" sizes="76x76" href="/static/favicon/apple-icon-76x76.png"> 20 - <link rel="apple-touch-icon" sizes="114x114" href="/static/favicon/apple-icon-114x114.png"> 21 - <link rel="apple-touch-icon" sizes="120x120" href="/static/favicon/apple-icon-120x120.png"> 22 - <link rel="apple-touch-icon" sizes="144x144" href="/static/favicon/apple-icon-144x144.png"> 23 - <link rel="apple-touch-icon" sizes="152x152" href="/static/favicon/apple-icon-152x152.png"> 24 - <link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-icon-180x180.png"> 25 - <link rel="icon" type="image/png" sizes="192x192" href="/static/favicon/android-icon-192x192.png"> 26 - <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png"> 27 - <link rel="icon" type="image/png" sizes="96x96" href="/static/favicon/favicon-96x96.png"> 28 - <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png"> 29 - <link rel="manifest" href="/static/favicon/manifest.json"> 30 - <meta name="msapplication-TileColor" content="#ffffff"> 31 - <meta name="msapplication-TileImage" content="/static/favicon/ms-icon-144x144.png"> 32 - <meta name="theme-color" content="#ffffff"> 33 - <style> 34 - .main { 35 - padding: 20px 10px; 36 - } 37 - 38 - .hack h1 { 39 - padding-top: 0; 40 - } 41 - 42 - footer.footer { 43 - border-top: 1px solid #ccc; 44 - margin-top: 80px; 45 - margin-top: 5rem; 46 - padding: 48px 0; 47 - padding: 3rem 0; 48 - } 49 - 50 - img { 51 - max-width: 100%; 52 - padding: 1em; 53 - } 54 - </style> 55 - {{ template "styles" . }} 56 - </head> 57 - <body class="snow hack gruvbox-dark"> 58 - {{ template "scripts" . }} 59 - <div class="container"> 60 - <header> 61 - <p><a href="/">Christine Dodrill</a> - <a href="/blog">Blog</a> - <a href="/contact">Contact</a> - <a href="/gallery">Gallery</a> - <a href="/resume">Resume</a> - <a href="/talks">Talks</a> - <a href="/signalboost">Signal Boost</a> - <a href="/feeds">Feeds</a> | <a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website">GraphViz</a> - <a target="_blank" rel="noopener noreferrer" href="https://when-then-zen.christine.website/">When Then Zen</a></p> 62 - </header> 63 - 64 - <div class="snowframe"> 65 - {{ template "content" . }} 66 - </div> 67 - 68 - <footer> 69 - <blockquote>Copyright 2020 Christine Dodrill. Any and all opinions listed here are my own and not representative of my employers; future, past and present.</blockquote> 70 - <br /> 71 - {{/* <p>Like what you see? Donate on <a href="https://www.patreon.com/cadey">Patreon</a> like <a href="/patrons">these awesome people</a>!</p> */}} 72 - <p>Looking for someone for your team? Take a look <a href="/signalboost">here</a>.</p> 73 - </footer> 74 - 75 - <script> 76 - if (navigator.serviceWorker.controller) { 77 - console.log("Active service worker found, no need to register"); 78 - } else { 79 - navigator.serviceWorker.register("/sw.js").then(function(reg) { 80 - console.log("Service worker has been registered for scope:" + reg.scope); 81 - }); 82 - } 83 - </script> 84 - </div> 85 - 86 - <script src="/static/js/instantpage-3.0.0.js" defer type="module"> </script> 87 - </body> 88 - </html> 89 - 90 - {{ define "scripts" }}{{ end }} 91 - {{ define "styles" }}{{ end }}
+10 -8
templates/blogindex.html templates/blogindex.rs.html
··· 1 - {{ define "title" }} 2 - <title>Blog - Christine Dodrill</title> 3 - {{ end }} 1 + @use crate::post::Post; 2 + @use super::{header_html, footer_html}; 4 3 5 - {{ define "content" }} 4 + @(posts: Vec<Post>) 5 + 6 + @:header_html(Some("Blog"), None) 7 + 6 8 <h1>Blogposts</h1> 7 9 8 10 <p>If you have a compatible reader, be sure to check out my <a href="/blog.rss">RSS Feed</a> for automatic updates. Also check out the <a href="/blog.json">JSONFeed</a>.</p> ··· 11 13 12 14 <p> 13 15 <ul> 14 - {{ range . }} 15 - <li>{{ .DateString }} - <a href="{{ .Link }}">{{ .Title }}</a></li> 16 - {{ end }} 16 + @for post in posts { 17 + <li>@post.date.format("%Y-%m-%d") - <a href="@post.link">@post.front_matter.title</a></li> 18 + } 17 19 </ul> 18 20 </p> 19 21 ··· 89 91 </blockquote> 90 92 </p> 91 93 92 - {{ end }} 94 + @:footer_html()
-115
templates/blogpost.html
··· 1 - {{ define "title" }} 2 - <title>{{ .Title }} - Christine Dodrill</title> 3 - 4 - <!-- Twitter --> 5 - <meta name="twitter:card" content="summary" /> 6 - <meta name="twitter:site" content="@theprincessxena" /> 7 - <meta name="twitter:title" content="{{ .Title }}" /> 8 - <meta name="twitter:description" content="Posted on {{ .Date }}" /> 9 - 10 - <!-- Facebook --> 11 - <meta property="og:type" content="website" /> 12 - <meta property="og:title" content="{{ .Title }}" /> 13 - <meta property="og:site_name" content="Christine Dodrill's Blog" /> 14 - 15 - <!-- Description --> 16 - <meta name="description" content="{{ .Title }} - Christine Dodrill's Blog" /> 17 - <meta name="author" content="Christine Dodrill"> 18 - 19 - <link rel="canonical" href="https://christine.website/{{ .Link }}"> 20 - 21 - <script type="application/ld+json"> 22 - { 23 - "@context": "http://schema.org", 24 - "@type": "Article", 25 - "headline": "{{ .Title }}", 26 - "image": "https://christine.website/static/img/avatar.png", 27 - "url": "https://christine.website/{{ .Link }}", 28 - "datePublished": "{{ .Date }}", 29 - "mainEntityOfPage": { 30 - "@type": "WebPage", 31 - "@id": "https://christine.website/{{ .Link }}" 32 - }, 33 - "author": { 34 - "@type": "Person", 35 - "name": "Christine Dodrill" 36 - }, 37 - "publisher": { 38 - "@type": "Person", 39 - "name": "Christine Dodrill" 40 - } 41 - } 42 - </script> 43 - {{ end }} 44 - 45 - {{ define "content" }} 46 - {{ .BodyHTML }} 47 - 48 - <hr /> 49 - 50 - <!-- The button that should be clicked. --> 51 - <button onclick="share_on_mastodon()">Share on Mastodon</button> 52 - 53 - <p>This article was posted on {{ .Date }}. Facts and circumstances may have changed since publication. Please <a href="/contact">contact me</a> before jumping to conclusions if something seems wrong or unclear.</p> 54 - 55 - {{ if ne .Series "" }} 56 - <p>Series: <a href="/blog/series/{{ .Series }}">{{ .Series }}</a></p> 57 - {{ end }} 58 - 59 - {{ if ne .Tags "" }} 60 - <p>Tags:{{.Tags}}</p> 61 - {{ end }} 62 - 63 - <script> 64 - 65 - // The actual function. Set this as an onclick function for your "Share on Mastodon" button 66 - function share_on_mastodon() { 67 - // Prefill the form with the user's previously-specified Mastodon instance, if applicable 68 - var default_url = localStorage['mastodon_instance']; 69 - 70 - // If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt. 71 - if (!default_url) 72 - default_url = "https://"; 73 - 74 - var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url); 75 - if (instance) { 76 - // Handle URL formats 77 - if ( !instance.startsWith("https://") && !instance.startsWith("http://") ) 78 - instance = "https://" + instance; 79 - 80 - // get the current page's url 81 - var url = window.location.href; 82 - 83 - // get the page title from the og:title meta tag, if it exists. 84 - var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content"); 85 - 86 - // Otherwise, use the <title> tag as the title 87 - if (!title) var title = document.getElementsByTagName("title")[0].innerHTML; 88 - 89 - // Handle slash 90 - if ( !instance.endsWith("/") ) 91 - instance = instance + "/"; 92 - 93 - // Cache the instance/domain for future requests 94 - localStorage['mastodon_instance'] = instance; 95 - 96 - // Hashtags 97 - var hashtags = "#blogpost"; 98 - 99 - {{ if ne .SeriesTag "" }}hashtags += " #{{ .SeriesTag }}";{{ end }} 100 - {{ if ne .Tags "" }}hashtags += "{{ .Tags }}";{{ end }} 101 - 102 - // Tagging users, such as offical accounts or the author of the post 103 - var author = "@cadey@mst3k.interlinked.me"; 104 - 105 - // Create the Share URL 106 - // https://someinstance.tld/share?text=URL%20encoded%20text 107 - mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author); 108 - 109 - // Open a new window at the share location 110 - window.open(mastodon_url, '_blank'); 111 - } 112 - } 113 - </script> 114 - 115 - {{ end }}
+122
templates/blogpost.rs.html
··· 1 + @use super::{header_html, footer_html}; 2 + @use crate::post::Post; 3 + 4 + @(post: Post, body: impl ToHtml) 5 + 6 + @:header_html(Some(&post.front_matter.title.clone()), None) 7 + 8 + <!-- Twitter --> 9 + <meta name="twitter:card" content="summary" /> 10 + <meta name="twitter:site" content="@@theprincessxena" /> 11 + <meta name="twitter:title" content="@post.front_matter.title" /> 12 + <meta name="twitter:description" content="Posted on @post.date" /> 13 + 14 + <!-- Facebook --> 15 + <meta property="og:type" content="website" /> 16 + <meta property="og:title" content="@post.front_matter.title" /> 17 + <meta property="og:site_name" content="Christine Dodrill's Blog" /> 18 + 19 + <!-- Description --> 20 + <meta name="description" content="@post.front_matter.title - Christine Dodrill's Blog" /> 21 + <meta name="author" content="Christine Dodrill"> 22 + 23 + <link rel="canonical" href="https://christine.website/@post.link"> 24 + 25 + <script type="application/ld+json"> 26 + @{ 27 + "@@context": "http://schema.org", 28 + "@@type": "Article", 29 + "headline": "@post.front_matter.title", 30 + "image": "https://christine.website/static/img/avatar.png", 31 + "url": "https://christine.website/@post.link", 32 + "datePublished": "@post.date", 33 + "mainEntityOfPage": @{ 34 + "@@type": "WebPage", 35 + "@@id": "https://christine.website/@post.link" 36 + @}, 37 + "author": @{ 38 + "@@type": "Person", 39 + "name": "Christine Dodrill" 40 + @}, 41 + "publisher": @{ 42 + "@@type": "Person", 43 + "name": "Christine Dodrill" 44 + @} 45 + @} 46 + </script> 47 + 48 + @body 49 + 50 + <hr /> 51 + 52 + <!-- The button that should be clicked. --> 53 + <button onclick="share_on_mastodon()">Share on Mastodon</button> 54 + 55 + <p>This article was posted on @post.detri(). Facts and circumstances may have changed since publication. Please <a href="/contact">contact me</a> before jumping to conclusions if something seems wrong or unclear.</p> 56 + 57 + @if post.front_matter.series.is_some() { 58 + <p>Series: <a href="/blog/series/@post.front_matter.series.as_ref().unwrap()">@post.front_matter.series.as_ref().unwrap()</a></p> 59 + } 60 + 61 + @if post.front_matter.tags.is_some() { 62 + <p>Tags: @for tag in post.front_matter.tags.as_ref().unwrap() { <code>@tag</code> }</p> 63 + } 64 + 65 + <script> 66 + 67 + // The actual function. Set this as an onclick function for your "Share on Mastodon" button 68 + function share_on_mastodon() @{ 69 + // Prefill the form with the user's previously-specified Mastodon instance, if applicable 70 + var default_url = localStorage['mastodon_instance']; 71 + 72 + // If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt. 73 + if (!default_url) 74 + default_url = "https://"; 75 + 76 + var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url); 77 + if (instance) @{ 78 + // Handle URL formats 79 + if ( !instance.startsWith("https://") && !instance.startsWith("http://") ) 80 + instance = "https://" + instance; 81 + 82 + // get the current page's url 83 + var url = window.location.href; 84 + 85 + // get the page title from the og:title meta tag, if it exists. 86 + var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content"); 87 + 88 + // Otherwise, use the <title> tag as the title 89 + if (!title) var title = document.getElementsByTagName("title")[0].innerHTML; 90 + 91 + // Handle slash 92 + if ( !instance.endsWith("/") ) 93 + instance = instance + "/"; 94 + 95 + // Cache the instance/domain for future requests 96 + localStorage['mastodon_instance'] = instance; 97 + 98 + // Hashtags 99 + var hashtags = "#blogpost"; 100 + 101 + @if post.front_matter.series.is_some() { 102 + hashtags += "#@post.front_matter.series.as_ref().unwrap()"; 103 + } 104 + 105 + @if post.front_matter.tags.is_some() { 106 + hashtags += "@for tag in post.front_matter.tags.as_ref().unwrap() { #@tag }"; 107 + } 108 + 109 + // Tagging users, such as offical accounts or the author of the post 110 + var author = "@@cadey@@mst3k.interlinked.me"; 111 + 112 + // Create the Share URL 113 + // https://someinstance.tld/share?text=URL%20encoded%20text 114 + mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author); 115 + 116 + // Open a new window at the share location 117 + window.open(mastodon_url, '_blank'); 118 + @} 119 + @} 120 + </script> 121 + 122 + @:footer_html()
+10 -7
templates/contact.html templates/contact.rs.html
··· 1 - {{ define "title" }}<title>Contact - Christine Dodrill</title>{{ end }} 1 + @use super::{header_html, footer_html}; 2 + 3 + @() 4 + 5 + @:header_html(Some("Contact"), None) 2 6 3 - {{ define "content" }} 4 7 <h1>Contact Information</h1> 5 8 <div class="grid"> 6 9 <div class="cell -6of12"> 7 10 <h3>Email</h3> 8 - <p>me@christine.website</p> 11 + <p>me@@christine.website</p> 9 12 10 13 <p>My GPG fingerprint is <code>799F 9134 8118 1111</code>. If you get an email that appears to be from me and the signature does not match that fingerprint, it is not from me. You may download a copy of my public key <a href="/static/gpg.pub">here</a>.</p> 11 14 ··· 14 17 <li><a href="https://github.com/Xe">Github</a></li> 15 18 <li><a href="https://twitter.com/theprincessxena">Twitter</a></li> 16 19 <li><a href="https://keybase.io/xena">Keybase</a></li> 17 - <li><a href="https://www.coinbase.com/christinedodrill">Coinbase</a></li> 18 20 <li><a href="https://ko-fi.com/A265JE0">Ko-fi</a></li> 19 21 <li><a href="https://www.patreon.com/cadey">Patreon</a></li> 20 22 <li><a href="https://www.facebook.com/chrissycade1337">Facebook</a></li> 21 - <li><a href="https://mst3k.interlinked.me/@cadey">@cadey@mst3k.interlinked.me</a></li> 23 + <li><a href="https://mst3k.interlinked.me/@@cadey">@@cadey@@mst3k.interlinked.me</a></li> 22 24 <li>Fortnite: Within Reason</li> 23 25 </ul> 24 26 </div> ··· 27 29 <p>I have a <a href="https://www.patreon.com/cadey">Patreon</a> if you want to send donations, otherwise my <a href="https://ko-fi.com/A265JE0">Ko-Fi</a> works too.</p> 28 30 29 31 <h4>Telegram</h4> 30 - <p><a href="https://t.me/miamorecadenza">@miamorecadenza</a></p> 32 + <p><a href="https://t.me/miamorecadenza">@@miamorecadenza</a></p> 31 33 32 34 <h4>Discord</h4> 33 35 <p><code>Cadey~#1337</code></p> 34 36 </div> 35 37 </div> 36 - {{ end }} 38 + 39 + @:footer_html()
-9
templates/error.html
··· 1 - {{ define "title" }} 2 - <title>Error - Christine Dodrill</title> 3 - {{ end }} 4 - 5 - {{ define "content" }} 6 - <pre> 7 - {{ . }} 8 - </pre> 9 - {{ end }}
+13
templates/error.rs.html
··· 1 + @use super::{header_html, footer_html}; 2 + 3 + @(why: String) 4 + 5 + @:header_html(Some("Error"), None) 6 + 7 + <h1>Error</h1> 8 + 9 + <code><pre>@why</pre></code> 10 + 11 + <p>You could try to <a href="/">go home</a> or <a href="https://github.com/Xe/site/issues/new">report this issue</a> so it can be fixed.</p> 12 + 13 + @:footer_html()
+6 -5
templates/feeds.html templates/feeds.rs.html
··· 1 - {{ define "title" }} 2 - <title>Feeds - Christine Dodrill</title> 3 - {{ end }} 1 + @use super::{header_html, footer_html}; 2 + 3 + @() 4 + 5 + @:header_html(Some("Feeds"), None) 4 6 5 - {{ define "content" }} 6 7 <h1>Feeds</h1> 7 8 8 9 <ul> ··· 11 12 <li>Mastodon: <a href="https://mst3k.interlinked.me/users/cadey.rss">RSS</a></li> 12 13 </ul> 13 14 14 - {{ end }} 15 + @:footer_html()
+17
templates/footer.rs.html
··· 1 + @use crate::APPLICATION_NAME as APP; 2 + 3 + @() 4 + </div> 5 + <hr /> 6 + <footer> 7 + <blockquote>Copyright 2020 Christine Dodrill. Any and all opinions listed here are my own and not representative of my employers; future, past and present.</blockquote> 8 + <!--<p>Like what you see? Donate on <a href="https://www.patreon.com/cadey">Patreon</a> like <a href="/patrons">these awesome people</a>!</p>--> 9 + <p>Looking for someone for your team? Take a look <a href="/signalboost">here</a>.</p> 10 + <p>Served by @APP running commit <a href="https://github.com/Xe/site/commit/@env!("GITHUB_SHA")">@env!("GITHUB_SHA")</a>, see <a href="https://github.com/Xe/site">source code here</a>.</p> 11 + </footer> 12 + 13 + </div> 14 + 15 + <script src="/static/js/installsw.js" defer></script> 16 + </body> 17 + </html>
-26
templates/galleryindex.html
··· 1 - {{ define "title" }} 2 - <title>Gallery - Christine Dodrill</title> 3 - <meta name="furbooru-validation" value="FUR-LINKVALIDATION-CD28668CBF" /> 4 - {{ end }} 5 - 6 - {{ define "content" }} 7 - <h1>Gallery</h1> 8 - 9 - <p>Here are links to all of the art I have done in the last few years.</p> 10 - 11 - <p>If you have a compatible reader, be sure to check out my <a href="/blog.rss">RSS Feed</a> for automatic updates. Also check out the <a href="/blog.json">JSONFeed</a>.</p> 12 - 13 - <p> 14 - <div class="grid"> 15 - {{ range . }} 16 - <div class="card cell -4of12 blogpost-card"> 17 - <header class="card-header">{{ .Title }}</header> 18 - <div class="card-content"> 19 - <center><p>Posted on {{ .DateString }} <br><a href="{{ .Link }}"><img src="{{ .ThumbURL }}" /></a></p></center> 20 - </div> 21 - </div> 22 - {{ end }} 23 - </div> 24 - </p> 25 - 26 - {{ end }}
+23
templates/galleryindex.rs.html
··· 1 + @use crate::post::Post; 2 + @use super::{header_html, footer_html}; 3 + 4 + @(posts: Vec<Post>) 5 + 6 + @:header_html(Some("Gallery"), None) 7 + 8 + <h1>Gallery</h1> 9 + 10 + <p>Here are links to a lot of the art I have done in the last few years.</p> 11 + 12 + <div class="grid"> 13 + @for post in posts { 14 + <div class="card cell -4of12 blogpost-card"> 15 + <header class="card-header">@post.front_matter.title</header> 16 + <div class="card-content"> 17 + <center><p>Posted on @post.date.format("%Y-%m-%d")<br /><a href="@post.link"><img src="@post.front_matter.thumb.as_ref().unwrap()" /></a></p></center> 18 + </div> 19 + </div> 20 + } 21 + </div> 22 + 23 + @:footer_html()
-116
templates/gallerypost.html
··· 1 - {{ define "title" }} 2 - <title>{{ .Title }} - Christine Dodrill</title> 3 - 4 - <!-- Twitter --> 5 - <meta name="twitter:card" content="summary" /> 6 - <meta name="twitter:site" content="@theprincessxena" /> 7 - <meta name="twitter:title" content="{{ .Title }}" /> 8 - <meta name="twitter:description" content="Posted on {{ .Date }}" /> 9 - 10 - <!-- Facebook --> 11 - <meta property="og:type" content="website" /> 12 - <meta property="og:title" content="{{ .Title }}" /> 13 - <meta property="og:site_name" content="Talk by Christine Dodrill" /> 14 - 15 - <!-- Description --> 16 - <meta name="description" content="{{ .Title }} - Talk by Christine Dodrill" /> 17 - <meta name="author" content="Christine Dodrill"> 18 - 19 - <link rel="canonical" href="https://christine.website/{{ .Link }}"> 20 - 21 - <script type="application/ld+json"> 22 - { 23 - "@context": "http://schema.org", 24 - "@type": "Painting", 25 - "headline": "{{ .Title }}", 26 - "image": "https://christine.website{{ .Image }}", 27 - "url": "https://christine.website/{{ .Link }}", 28 - "datePublished": "{{ .Date }}", 29 - "mainEntityOfPage": { 30 - "@type": "", 31 - "@id": "https://christine.website{{ .Image }}" 32 - }, 33 - "creator": { 34 - "@type": "Person", 35 - "name": "Christine Dodrill" 36 - }, 37 - "publisher": { 38 - "@type": "Person", 39 - "name": "Christine Dodrill" 40 - } 41 - } 42 - </script> 43 - {{ end }} 44 - 45 - {{ define "content" }} 46 - 47 - <h1>{{ .Title }}</h1> 48 - 49 - {{ .BodyHTML }} 50 - 51 - <center> 52 - <img src="{{ .Image }}" /> 53 - </center> 54 - 55 - <hr /> 56 - 57 - <!-- The button that should be clicked. --> 58 - <button onclick="share_on_mastodon()">Share on Mastodon</button> 59 - 60 - <p>This artwork was posted on {{ .Date }}.</p> 61 - 62 - {{ if ne .Tags "" }} 63 - <p>Tags:{{.Tags}}</p> 64 - {{ end }} 65 - 66 - <script> 67 - 68 - // The actual function. Set this as an onclick function for your "Share on Mastodon" button 69 - function share_on_mastodon() { 70 - // Prefill the form with the user's previously-specified Mastodon instance, if applicable 71 - var default_url = localStorage['mastodon_instance']; 72 - 73 - // If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt. 74 - if (!default_url) 75 - default_url = "https://"; 76 - 77 - var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url); 78 - if (instance) { 79 - // Handle URL formats 80 - if ( !instance.startsWith("https://") && !instance.startsWith("http://") ) 81 - instance = "https://" + instance; 82 - 83 - // Get the current page's URL 84 - var url = window.location.href; 85 - 86 - // Get the page title from the og:title meta tag, if it exists. 87 - var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content"); 88 - 89 - // Otherwise, use the <title> tag as the title 90 - if (!title) var title = document.getElementsByTagName("title")[0].innerHTML; 91 - 92 - // Handle slash 93 - if ( !instance.endsWith("/") ) 94 - instance = instance + "/"; 95 - 96 - // Cache the instance/domain for future requests 97 - localStorage['mastodon_instance'] = instance; 98 - 99 - // Hashtags 100 - var hashtags = "#art"; 101 - {{ if ne .Tags "" }}hashtags += " {{ .Tags }}";{{ end }} 102 - 103 - // Tagging users, such as offical accounts or the author of the post 104 - var author = "@cadey@mst3k.interlinked.me"; 105 - 106 - // Create the Share URL 107 - // https://someinstance.tld/share?text=URL%20encoded%20text 108 - mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author); 109 - 110 - // Open a new window at the share location 111 - window.open(mastodon_url, '_blank'); 112 - } 113 - } 114 - </script> 115 - 116 - {{ end }}
+124
templates/gallerypost.rs.html
··· 1 + @use super::{header_html, footer_html}; 2 + @use crate::post::Post; 3 + 4 + @(post: Post, body: impl ToHtml) 5 + 6 + @:header_html(Some(&post.front_matter.title.clone()), None) 7 + 8 + <!-- Twitter --> 9 + <meta name="twitter:card" content="summary" /> 10 + <meta name="twitter:site" content="@@theprincessxena" /> 11 + <meta name="twitter:title" content="@post.front_matter.title" /> 12 + <meta name="twitter:description" content="Posted on @post.date" /> 13 + 14 + <!-- Facebook --> 15 + <meta property="og:type" content="website" /> 16 + <meta property="og:title" content="@post.front_matter.title" /> 17 + <meta property="og:site_name" content="Christine Dodrill's Blog" /> 18 + 19 + <!-- Description --> 20 + <meta name="description" content="@post.front_matter.title - Christine Dodrill's Blog" /> 21 + <meta name="author" content="Christine Dodrill"> 22 + 23 + <link rel="canonical" href="https://christine.website/@post.link"> 24 + 25 + <script type="application/ld+json"> 26 + @{ 27 + "@@context": "http://schema.org", 28 + "@@type": "Article", 29 + "headline": "@post.front_matter.title", 30 + "image": "https://christine.website/static/img/avatar.png", 31 + "url": "https://christine.website/@post.link", 32 + "datePublished": "@post.date", 33 + "mainEntityOfPage": @{ 34 + "@@type": "WebPage", 35 + "@@id": "https://christine.website/@post.link" 36 + @}, 37 + "author": @{ 38 + "@@type": "Person", 39 + "name": "Christine Dodrill" 40 + @}, 41 + "publisher": @{ 42 + "@@type": "Person", 43 + "name": "Christine Dodrill" 44 + @} 45 + @} 46 + </script> 47 + 48 + <h1>@post.front_matter.title</h1> 49 + 50 + @body 51 + 52 + <center> 53 + <img src="@post.front_matter.image.as_ref().unwrap()" /> 54 + </center> 55 + 56 + <hr /> 57 + 58 + <!-- The button that should be clicked. --> 59 + <button onclick="share_on_mastodon()">Share on Mastodon</button> 60 + 61 + <p>This artwork was posted on @post.detri().</p> 62 + 63 + @if post.front_matter.series.is_some() { 64 + <p>Series: <a href="/blog/series/@post.front_matter.series.as_ref().unwrap()">@post.front_matter.series.as_ref().unwrap()</a></p> 65 + } 66 + 67 + @if post.front_matter.tags.is_some() { 68 + <p>Tags: @for tag in post.front_matter.tags.as_ref().unwrap() { <code>@tag</code> }</p> 69 + } 70 + 71 + <script> 72 + 73 + // The actual function. Set this as an onclick function for your "Share on Mastodon" button 74 + function share_on_mastodon() @{ 75 + // Prefill the form with the user's previously-specified Mastodon instance, if applicable 76 + var default_url = localStorage['mastodon_instance']; 77 + 78 + // If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt. 79 + if (!default_url) 80 + default_url = "https://"; 81 + 82 + var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url); 83 + if (instance) @{ 84 + // Handle URL formats 85 + if ( !instance.startsWith("https://") && !instance.startsWith("http://") ) 86 + instance = "https://" + instance; 87 + 88 + // get the current page's url 89 + var url = window.location.href; 90 + 91 + // get the page title from the og:title meta tag, if it exists. 92 + var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content"); 93 + 94 + // Otherwise, use the <title> tag as the title 95 + if (!title) var title = document.getElementsByTagName("title")[0].innerHTML; 96 + 97 + // Handle slash 98 + if ( !instance.endsWith("/") ) 99 + instance = instance + "/"; 100 + 101 + // Cache the instance/domain for future requests 102 + localStorage['mastodon_instance'] = instance; 103 + 104 + // Hashtags 105 + var hashtags = "#art"; 106 + 107 + @if post.front_matter.tags.is_some() { 108 + hashtags += "@for tag in post.front_matter.tags.as_ref().unwrap() { #@tag}"; 109 + } 110 + 111 + // Tagging users, such as offical accounts or the author of the post 112 + var author = "@@cadey@@mst3k.interlinked.me"; 113 + 114 + // Create the Share URL 115 + // https://someinstance.tld/share?text=URL%20encoded%20text 116 + mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author); 117 + 118 + // Open a new window at the share location 119 + window.open(mastodon_url, '_blank'); 120 + @} 121 + @} 122 + </script> 123 + 124 + @:footer_html()
+52
templates/header.rs.html
··· 1 + @use chrono::{Datelike, Utc}; 2 + 3 + @(title: Option<&str>, styles: Option<&str>) 4 + 5 + <!DOCTYPE html> 6 + <html lang="en"> 7 + <head> 8 + @if title.is_some() { 9 + <title>@title.unwrap() - Christine Dodrill</title> 10 + } else { 11 + <title>Christine Dodrill</title> 12 + } 13 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 14 + <link rel="stylesheet" href="/css/hack.css" /> 15 + <link rel="stylesheet" href="/css/gruvbox-dark.css" /> 16 + <link rel="stylesheet" href="/css/shim.css" /> 17 + @if Utc::now().month() == 12 { <link rel="stylesheet" href="/css/snow.css" /> } 18 + <link rel="manifest" href="/static/manifest.json" /> 19 + 20 + <link rel="alternate" title="Christine Dodrill's Blog" type="application/rss+xml" href="https://christine.website/blog.rss" /> 21 + <link rel="alternate" title="Christine Dodrill's Blog" type="application/json" href="https://christine.website/blog.json" /> 22 + 23 + <link rel="apple-touch-icon" sizes="57x57" href="/static/favicon/apple-icon-57x57.png"> 24 + <link rel="apple-touch-icon" sizes="60x60" href="/static/favicon/apple-icon-60x60.png"> 25 + <link rel="apple-touch-icon" sizes="72x72" href="/static/favicon/apple-icon-72x72.png"> 26 + <link rel="apple-touch-icon" sizes="76x76" href="/static/favicon/apple-icon-76x76.png"> 27 + <link rel="apple-touch-icon" sizes="114x114" href="/static/favicon/apple-icon-114x114.png"> 28 + <link rel="apple-touch-icon" sizes="120x120" href="/static/favicon/apple-icon-120x120.png"> 29 + <link rel="apple-touch-icon" sizes="144x144" href="/static/favicon/apple-icon-144x144.png"> 30 + <link rel="apple-touch-icon" sizes="152x152" href="/static/favicon/apple-icon-152x152.png"> 31 + <link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-icon-180x180.png"> 32 + <link rel="icon" type="image/png" sizes="192x192" href="/static/favicon/android-icon-192x192.png"> 33 + <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png"> 34 + <link rel="icon" type="image/png" sizes="96x96" href="/static/favicon/favicon-96x96.png"> 35 + <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png"> 36 + <link rel="manifest" href="/static/favicon/manifest.json"> 37 + <meta name="msapplication-TileColor" content="#ffffff"> 38 + <meta name="msapplication-TileImage" content="/static/favicon/ms-icon-144x144.png"> 39 + <meta name="theme-color" content="#ffffff"> 40 + @if styles.is_some() { 41 + <style> 42 + @styles.unwrap() 43 + </style> 44 + } 45 + </head> 46 + <body class="snow hack gruvbox-dark"> 47 + <div class="container"> 48 + <header> 49 + <p><a href="/">Christine Dodrill</a> - <a href="/blog">Blog</a> - <a href="/contact">Contact</a> - <a href="/gallery">Gallery</a> - <a href="/resume">Resume</a> - <a href="/talks">Talks</a> - <a href="/signalboost">Signal Boost</a> - <a href="/feeds">Feeds</a> | <a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website">GraphViz</a> - <a target="_blank" rel="noopener noreferrer" href="https://when-then-zen.christine.website/">When Then Zen</a></p> 50 + </header> 51 + 52 + <div class="snowframe">
+15 -12
templates/index.html templates/index.rs.html
··· 1 - {{ define "title" }} 2 - <title>Christine Dodrill</title> 1 + @use super::{header_html, footer_html}; 2 + 3 + @() 4 + 5 + @:header_html(None, None) 6 + 3 7 <link rel="authorization_endpoint" href="https://idp.christine.website/auth"> 4 8 <link rel="canonical" href="https://christine.website/"> 5 9 <meta name="google-site-verification" content="rzs9eBEquMYr9Phrg0Xm0mIwFjDBcbdgJ3jF6Disy-k" /> 6 10 <script type="application/ld+json"> 7 - { 8 - "@context": "http://schema.org/", 9 - "@type": "Person", 11 + @{ 12 + "@@context": "http://schema.org/", 13 + "@@type": "Person", 10 14 "name": "Christine Dodrill", 11 15 "alternateName": "Cadey, Xe, Xena", 12 16 "url": "https://christine.website", ··· 15 19 "https://github.com/Xe", 16 20 "https://git.xeserv.us/xena", 17 21 "https://twitter.com/theprincessxena", 18 - "https://mst3k.interlinked.me/@cadey", 22 + "https://mst3k.interlinked.me/@@cadey", 19 23 "https://www.linkedin.com/in/christine-dodrill-1827a010b/", 20 24 "https://www.youtube.com/user/shadowh511" 21 25 ] 22 - } 26 + @} 23 27 </script> 24 28 25 29 <!-- Twitter --> 26 30 <meta name="twitter:card" content="summary" /> 27 - <meta name="twitter:site" content="@theprincessxena" /> 31 + <meta name="twitter:site" content="@@theprincessxena" /> 28 32 <meta name="twitter:title" content="Christine Dodrill" /> 29 33 <meta name="twitter:description" content="Full-stack Engineer" /> 30 34 ··· 36 40 <!-- Description --> 37 41 <meta name="description" content="Full-stack Engineer" /> 38 42 <meta name="author" content="Christine Dodrill"> 39 - {{ end }} 40 43 41 - {{ define "content" }} 42 44 <div class="grid"> 43 45 <div class="cell -3of12 content"> 44 46 <img src="/static/img/avatar.png" alt="My Avatar"> ··· 78 80 <ul> 79 81 <li><a href="https://github.com/Xe" rel="me">GitHub</a></li> 80 82 <li><a href="https://twitter.com/theprincessxena" rel="me">Twitter</a></li> 81 - <li><a href="https://mst3k.interlinked.me/@cadey" rel="me">Mastodon</a></li> 83 + <li><a href="https://mst3k.interlinked.me/@@cadey" rel="me">Mastodon</a></li> 82 84 <li><a href="https://www.patreon.com/cadey" rel="me">Patreon</a></li> 83 85 </ul> 84 86 85 87 <p>Looking for someone for your team? Check <a href="/signalboost">here</a>. 86 88 </div> 87 89 </div> 88 - {{ end }} 90 + 91 + @:footer_html()
+11
templates/notfound.rs.html
··· 1 + @use super::{header_html, footer_html}; 2 + 3 + @(path: String) 4 + 5 + @:header_html(Some("Not Found"), None) 6 + 7 + <h1>Not Found</h1> 8 + 9 + <p>The path at <code>@path</code> could not be found. If you expected this path to exist, please <a href="https://github.com/Xe/site/issues/new">report this issue</a> so it can be fixed.</p> 10 + 11 + @:footer_html()
-20
templates/patrons.html
··· 1 - {{ define "title" }} 2 - <title>Patrons - Christine Dodrill</title> 3 - {{ end }} 4 - 5 - {{ define "content" }} 6 - <h1>Patrons</h1> 7 - 8 - <p>These awesome people donate to me on <a href="https://patreon.com/cadey">Patreon</a>. If you would like to show up in this list, please donate to me on Patreon. This is refreshed every time the site is deployed.</p> 9 - 10 - <p> 11 - <ul> 12 - {{- range . }} 13 - <li>{{ . }}</li> 14 - {{- end }} 15 - </ul> 16 - </p> 17 - 18 - <p>Thank you so much! Your support helps make my projects possible.</p> 19 - 20 - {{ end }}
+20
templates/patrons.rs.html
··· 1 + @use patreon::Users; 2 + @use super::{header_html, footer_html}; 3 + 4 + @(users: Users) 5 + 6 + @:header_html(Some("Patrons"), None) 7 + 8 + <h1>Patrons</h1> 9 + 10 + <p>These awesome people donate to me on <a href="https://patreon.com/cadey">Patreon</a>. If you would like to show up in this list, please donate to me on Patreon. This is refreshed every time the site is deployed.</p> 11 + 12 + <p> 13 + <ul> 14 + @for user in users { 15 + <li>@user.attributes.full_name</li> 16 + } 17 + </ul> 18 + </p> 19 + 20 + @:footer_html()
-9
templates/resume.html
··· 1 - {{ define "title" }}<title>Resume - Christine Dodrill</title>{{ end }} 2 - 3 - {{ define "content" }} 4 - {{ . }} 5 - 6 - <hr /> 7 - 8 - <a href="/static/resume/resume.md">Plain-text version of this resume here</a> 9 - {{ end }}
+13
templates/resume.rs.html
··· 1 + @use super::{header_html, footer_html}; 2 + 3 + @(resume: impl ToHtml) 4 + 5 + @:header_html(Some("Resume"), None) 6 + 7 + @resume 8 + 9 + <hr /> 10 + 11 + <a href="/static/resume/resume.md">Plain-text version of this resume here</a> 12 + 13 + @:footer_html()
-16
templates/series.html
··· 1 - {{ define "title" }} 2 - <title>Blog Series - Christine Dodrill</title> 3 - {{ end }} 4 - 5 - {{ define "content" }} 6 - <h1>Series</h1> 7 - 8 - <p> 9 - <ul> 10 - {{ range . }} 11 - <li><a href="/blog/series/{{ . }}">{{ . }}</a></li> 12 - {{ end }} 13 - </ul> 14 - </p> 15 - 16 - {{ end }}
+17
templates/series.rs.html
··· 1 + @use super::{header_html, footer_html}; 2 + 3 + @(series: Vec<String>) 4 + 5 + @:header_html(Some("Blog"), None) 6 + 7 + <h1>Post Series</h1> 8 + 9 + <p> 10 + <ul> 11 + @for set in series { 12 + <li><a href="/blog/series/@set">@set</a></li> 13 + } 14 + </ul> 15 + </p> 16 + 17 + @:footer_html()
+18
templates/series_posts.rs.html
··· 1 + @use crate::post::Post; 2 + @use super::{header_html, footer_html}; 3 + 4 + @(name: String, posts: &Vec<Post>) 5 + 6 + @:header_html(Some(&name), None) 7 + 8 + <h1>Series: @name</h1> 9 + 10 + <p> 11 + <ul> 12 + @for post in posts { 13 + <li>@post.date - <a href="/@post.link">@post.front_matter.title</a></li> 14 + } 15 + </ul> 16 + </p> 17 + 18 + @:footer_html()
-16
templates/serieslist.html
··· 1 - {{ define "title" }} 2 - <title>Blog {{.Name}} - Christine Dodrill</title> 3 - {{ end }} 4 - 5 - {{ define "content" }} 6 - <h1>Series: {{ .Name }}</h1> 7 - 8 - <p> 9 - <ul> 10 - {{ range .Posts }} 11 - <li>{{ .DateString }} - <a href="/{{ .Link }}">{{ .Title }}</a></li> 12 - {{ end }} 13 - </ul> 14 - </p> 15 - 16 - {{ end }}
-30
templates/signalboost.html
··· 1 - {{ define "title" }} 2 - <title>Signal Boosts - Christine Dodrill</title> 3 - {{ end }} 4 - 5 - {{ define "content" }} 6 - <h1>Signal Boosts</h1> 7 - 8 - <p>These awesome people are currently looking for a job. If you are looking for anyone with these skills, please feel free to reach out to them.</p> 9 - 10 - <p>To add yourself to this list, fork <a href="https://github.com/Xe/site">this website's source code</a> and send a pull request with edits to <code>signalboost.dhall</code>.</p> 11 - 12 - {{/* Remove this after COVID-19 is less of a thing */}} 13 - 14 - <p>With COVID-19 raging across the world, these people are in need of a job now more than ever.</p> 15 - 16 - {{/* end COVID-19 note */}} 17 - 18 - <h2>People</h2> 19 - 20 - <div class="grid signalboost"> 21 - {{ range . }} 22 - <div class="cell -4of12 content"> 23 - <big>{{ .Name }}</big> 24 - <p>{{ range .Tags }}{{ . }} {{ end }}</p> 25 - <a href="{{ .GitLink }}">GitHub</a> - <a href="{{ .Twitter }}">Twitter</a> 26 - </div> 27 - {{ end }} 28 - </div> 29 - 30 - {{ end }}
+29
templates/signalboost.rs.html
··· 1 + @use super::{header_html, footer_html}; 2 + @use crate::signalboost::Person; 3 + 4 + @(people: Vec<Person>) 5 + 6 + @:header_html(Some("Signal Boosts"), None) 7 + 8 + <h1>Signal Boosts</h1> 9 + 10 + <p>These awesome people are currently looking for a job. If you are looking for anyone with these skills, please feel free to reach out to them.</p> 11 + 12 + <p>To add yourself to this list, fork <a href="https://github.com/Xe/site">this website's source code</a> and send a pull request with edits to <code>signalboost.dhall</code>.</p> 13 + 14 + <!-- TODO(Xe): Remove this after COVID-19 is less of a thing --> 15 + <p>With COVID-19 raging across the world, these people are in need of a job now more than ever.</p> 16 + 17 + <h2>People</h2> 18 + 19 + <div class="grid signalboost"> 20 + @for person in people { 21 + <div class="cell -4of12 content"> 22 + <big>@person.name</big> 23 + <p>@for tag in person.tags { @tag }</p> 24 + <a href="@person.git_link">GitHub</a> - <a href="@person.twitter">Twitter</a> 25 + </div> 26 + } 27 + </div> 28 + 29 + @:footer_html()
-20
templates/talkindex.html
··· 1 - {{ define "title" }} 2 - <title>Talks - Christine Dodrill</title> 3 - {{ end }} 4 - 5 - {{ define "content" }} 6 - <h1>Talks</h1> 7 - 8 - <p>Here is a link to all of the talks I have done at conferences. Each of these will have links to the slides (PDF) as well as some brief information about them.</p> 9 - 10 - <p>If you have a compatible reader, be sure to check out my <a href="/blog.rss">RSS Feed</a> for automatic updates. Also check out the <a href="/blog.json">JSONFeed</a>.</p> 11 - 12 - <p> 13 - <ul> 14 - {{ range . }} 15 - <li>{{ .DateString }} - <a href="{{ .Link }}">{{ .Title }}</a></li> 16 - {{ end }} 17 - </ul> 18 - </p> 19 - 20 - {{ end }}
+23
templates/talkindex.rs.html
··· 1 + 2 + @use crate::post::Post; 3 + @use super::{header_html, footer_html}; 4 + 5 + @(posts: Vec<Post>) 6 + 7 + @:header_html(Some("Talks"), None) 8 + 9 + <h1>Talks</h1> 10 + 11 + <p>Here is a link to all of the talks I have done at conferences. Each of these will have links to the slides (PDF) as well as some brief information about them.</p> 12 + 13 + <p>If you have a compatible reader, be sure to check out my <a href="/blog.rss">RSS Feed</a> for automatic updates. Also check out the <a href="/blog.json">JSONFeed</a>.</p> 14 + 15 + <p> 16 + <ul> 17 + @for post in posts { 18 + <li>@post.date.format("%Y-%m-%d") - <a href="@post.link">@post.front_matter.title</a></li> 19 + } 20 + </ul> 21 + </p> 22 + 23 + @:footer_html()
-106
templates/talkpost.html
··· 1 - {{ define "title" }} 2 - <title>{{ .Title }} - Christine Dodrill</title> 3 - 4 - <!-- Twitter --> 5 - <meta name="twitter:card" content="summary" /> 6 - <meta name="twitter:site" content="@theprincessxena" /> 7 - <meta name="twitter:title" content="{{ .Title }}" /> 8 - <meta name="twitter:description" content="Posted on {{ .Date }}" /> 9 - 10 - <!-- Facebook --> 11 - <meta property="og:type" content="website" /> 12 - <meta property="og:title" content="{{ .Title }}" /> 13 - <meta property="og:site_name" content="Talk by Christine Dodrill" /> 14 - 15 - <!-- Description --> 16 - <meta name="description" content="{{ .Title }} - Talk by Christine Dodrill" /> 17 - <meta name="author" content="Christine Dodrill"> 18 - 19 - <link rel="canonical" href="https://christine.website/{{ .Link }}"> 20 - 21 - <script type="application/ld+json"> 22 - { 23 - "@context": "http://schema.org", 24 - "@type": "Article", 25 - "headline": "{{ .Title }}", 26 - "image": "https://christine.website/static/img/avatar.png", 27 - "url": "https://christine.website/{{ .Link }}", 28 - "datePublished": "{{ .Date }}", 29 - "mainEntityOfPage": { 30 - "@type": "WebPage", 31 - "@id": "https://christine.website/{{ .Link }}" 32 - }, 33 - "author": { 34 - "@type": "Person", 35 - "name": "Christine Dodrill" 36 - }, 37 - "publisher": { 38 - "@type": "Person", 39 - "name": "Christine Dodrill" 40 - } 41 - } 42 - </script> 43 - {{ end }} 44 - 45 - {{ define "content" }} 46 - {{ .BodyHTML }} 47 - 48 - <a href="{{ .SlidesLink }}">Link to the slides</a> 49 - 50 - <hr /> 51 - 52 - <!-- The button that should be clicked. --> 53 - <button onclick="share_on_mastodon()">Share on Mastodon</button> 54 - 55 - <p>This article was posted on {{ .Date }}. Facts and circumstances may have changed since publication. Please <a href="/contact">contact me</a> before jumping to conclusions if something seems wrong or unclear.</p> 56 - 57 - <script> 58 - 59 - // The actual function. Set this as an onclick function for your "Share on Mastodon" button 60 - function share_on_mastodon() { 61 - // Prefill the form with the user's previously-specified Mastodon instance, if applicable 62 - var default_url = localStorage['mastodon_instance']; 63 - 64 - // If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt. 65 - if (!default_url) 66 - default_url = "https://"; 67 - 68 - var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url); 69 - if (instance) { 70 - // Handle URL formats 71 - if ( !instance.startsWith("https://") && !instance.startsWith("http://") ) 72 - instance = "https://" + instance; 73 - 74 - // Get the current page's URL 75 - var url = window.location.href; 76 - 77 - // Get the page title from the og:title meta tag, if it exists. 78 - var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content"); 79 - 80 - // Otherwise, use the <title> tag as the title 81 - if (!title) var title = document.getElementsByTagName("title")[0].innerHTML; 82 - 83 - // Handle slash 84 - if ( !instance.endsWith("/") ) 85 - instance = instance + "/"; 86 - 87 - // Cache the instance/domain for future requests 88 - localStorage['mastodon_instance'] = instance; 89 - 90 - // Hashtags 91 - var hashtags = "#talk"; 92 - 93 - // Tagging users, such as offical accounts or the author of the post 94 - var author = "@cadey@mst3k.interlinked.me"; 95 - 96 - // Create the Share URL 97 - // https://someinstance.tld/share?text=URL%20encoded%20text 98 - mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author); 99 - 100 - // Open a new window at the share location 101 - window.open(mastodon_url, '_blank'); 102 - } 103 - } 104 - </script> 105 - 106 - {{ end }}
+120
templates/talkpost.rs.html
··· 1 + @use super::{header_html, footer_html}; 2 + @use crate::post::Post; 3 + 4 + @(post: Post, body: impl ToHtml) 5 + 6 + @:header_html(Some(&post.front_matter.title.clone()), None) 7 + 8 + <!-- Twitter --> 9 + <meta name="twitter:card" content="summary" /> 10 + <meta name="twitter:site" content="@@theprincessxena" /> 11 + <meta name="twitter:title" content="@post.front_matter.title" /> 12 + <meta name="twitter:description" content="Posted on @post.date" /> 13 + 14 + <!-- Facebook --> 15 + <meta property="og:type" content="website" /> 16 + <meta property="og:title" content="@post.front_matter.title" /> 17 + <meta property="og:site_name" content="Christine Dodrill's Blog" /> 18 + 19 + <!-- Description --> 20 + <meta name="description" content="@post.front_matter.title - Christine Dodrill's Blog" /> 21 + <meta name="author" content="Christine Dodrill"> 22 + 23 + <link rel="canonical" href="https://christine.website/@post.link"> 24 + 25 + <script type="application/ld+json"> 26 + @{ 27 + "@@context": "http://schema.org", 28 + "@@type": "Article", 29 + "headline": "@post.front_matter.title", 30 + "image": "https://christine.website/static/img/avatar.png", 31 + "url": "https://christine.website/@post.link", 32 + "datePublished": "@post.date", 33 + "mainEntityOfPage": @{ 34 + "@@type": "WebPage", 35 + "@@id": "https://christine.website/@post.link" 36 + @}, 37 + "author": @{ 38 + "@@type": "Person", 39 + "name": "Christine Dodrill" 40 + @}, 41 + "publisher": @{ 42 + "@@type": "Person", 43 + "name": "Christine Dodrill" 44 + @} 45 + @} 46 + </script> 47 + 48 + @body 49 + 50 + <a href="@post.front_matter.slides_link.as_ref().unwrap()">Link to the slides</a> 51 + 52 + <hr /> 53 + 54 + <!-- The button that should be clicked. --> 55 + <button onclick="share_on_mastodon()">Share on Mastodon</button> 56 + 57 + <p>This article was posted on @post.detri(). Facts and circumstances may have changed since publication. Please <a href="/contact">contact me</a> before jumping to conclusions if something seems wrong or unclear.</p> 58 + 59 + @if post.front_matter.series.is_some() { 60 + <p>Series: <a href="/blog/series/@post.front_matter.series.as_ref().unwrap()">@post.front_matter.series.as_ref().unwrap()</a></p> 61 + } 62 + 63 + @if post.front_matter.tags.is_some() { 64 + <p>Tags: @for tag in post.front_matter.tags.as_ref().unwrap() { <code>@tag</code> }</p> 65 + } 66 + 67 + <script> 68 + 69 + // The actual function. Set this as an onclick function for your "Share on Mastodon" button 70 + function share_on_mastodon() @{ 71 + // Prefill the form with the user's previously-specified Mastodon instance, if applicable 72 + var default_url = localStorage['mastodon_instance']; 73 + 74 + // If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt. 75 + if (!default_url) 76 + default_url = "https://"; 77 + 78 + var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url); 79 + if (instance) @{ 80 + // Handle URL formats 81 + if ( !instance.startsWith("https://") && !instance.startsWith("http://") ) 82 + instance = "https://" + instance; 83 + 84 + // get the current page's url 85 + var url = window.location.href; 86 + 87 + // get the page title from the og:title meta tag, if it exists. 88 + var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content"); 89 + 90 + // Otherwise, use the <title> tag as the title 91 + if (!title) var title = document.getElementsByTagName("title")[0].innerHTML; 92 + 93 + // Handle slash 94 + if ( !instance.endsWith("/") ) 95 + instance = instance + "/"; 96 + 97 + // Cache the instance/domain for future requests 98 + localStorage['mastodon_instance'] = instance; 99 + 100 + // Hashtags 101 + var hashtags = "#talk"; 102 + 103 + @if post.front_matter.tags.is_some() { 104 + hashtags += "@for tag in post.front_matter.tags.as_ref().unwrap() { #@tag}"; 105 + } 106 + 107 + // Tagging users, such as offical accounts or the author of the post 108 + var author = "@@cadey@@mst3k.interlinked.me"; 109 + 110 + // Create the Share URL 111 + // https://someinstance.tld/share?text=URL%20encoded%20text 112 + mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author); 113 + 114 + // Open a new window at the share location 115 + window.open(mastodon_url, '_blank'); 116 + @} 117 + @} 118 + </script> 119 + 120 + @:footer_html()