An experimental IndieWeb site built in Go.

Compare changes

Choose any two refs to compare.

+7
.dockerignore
··· 1 + data 2 + .git 3 + .gitignore 4 + .gitkeep 5 + *.md 6 + *.db 7 + node_modules
+17
.env.example
··· 1 + # Port for the server to listen on. 2 + # Change this to something you can tunnel during local development 3 + PORT=5050 4 + 5 + # Root URL, with protocol, where your server is accessible. 6 + PROFILE_URL="http://localhost/" 7 + 8 + ADMIN_USERNAME="change" 9 + ADMIN_PASSWORD="me" 10 + 11 + JWT_SECRET="bogus" 12 + 13 + AWS_ACCESS_KEY_ID= 14 + AWS_SECRET_ACCESS_KEY= 15 + AWS_S3_BUCKET_NAME= 16 + AWS_S3_ENDPOINT=fly.storage.tigris.dev 17 + AWS_REGION=auto
+1
.gitattributes
··· 1 + *.sh text eol=lf
+18
.github/workflows/fly-deploy.yml
··· 1 + # See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ 2 + 3 + name: Fly Deploy 4 + on: 5 + push: 6 + branches: 7 + - main 8 + jobs: 9 + deploy: 10 + name: Deploy app 11 + runs-on: ubuntu-latest 12 + concurrency: deploy-group # optional: ensure only one action runs at a time 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: superfly/flyctl-actions/setup-flyctl@master 16 + - run: flyctl deploy --remote-only --config ./config/fly.toml 17 + env: 18 + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+5 -1
.gitignore
··· 1 1 .env 2 2 3 3 node_modules 4 - *_templ.go 4 + 5 5 data/**/*.db 6 + tmp/ 7 + *_templ.txt 8 + 9 + static/styles.css
+47
README.md
··· 1 + # Space 2 + 3 + > A likely-to-be-unfinished experimental IndieWeb site, built with Go. 4 + 5 + Space is an attempt at building an IndieWeb-capable personal site, before I 6 + started looking into AT Protocol. It uses [Chi](https://go-chi.io/#/), 7 + [Templ](https://templ.guide/), and [TailwindCSS](https://tailwindcss.com). 8 + Data is stored using SQLite, and your DB is continuously backed up to S3 via 9 + [Litestream](https://litestream.io/). 10 + 11 + Any unrecognized or unsupported [post types](https://indieweb.org/posts) are 12 + rendered as raw JSON until they are explicitly supported. Media files are 13 + stored in the same S3 backend that Litestream uses, so all your data is 14 + replicated off-machine. 15 + 16 + Notably, the Tailwind pipeline is not checked into the project, as this was 17 + built before Go Tool support. 18 + 19 + If you want to take it for a test-drive, you can check the `.env.example` file 20 + for all required configuration values. 21 + 22 + ## Supported Specs 23 + 24 + - [x] [IndieAuth](https://www.w3.org/TR/indieauth/) via Basic Auth 25 + - [x] [Micropub](https://www.w3.org/TR/micropub/) 26 + - [ ] [h-card](https://microformats.org/wiki/h-card) 27 + - [ ] [Webmentions](https://www.w3.org/TR/webmention/) 28 + - [ ] [Microsub](https://indieweb.org/Microsub-spec) 29 + 30 + ## What happened? 31 + 32 + At the time, I was quite pleased with how this turned out. I think I nailed the 33 + aesthetics, and I hit my requirement for data durability and replication. I was 34 + targeting Fly.io for deployment, and Fly+Tigris was an excellent combo. 35 + 36 + Day-to-day, though, I couldn't make a habit of using this. IndieWeb is a lonely 37 + place when you don't have any connections and you're not a social creature by 38 + default. Implementing support for different post types over time felt like it 39 + was going to be a drag, and my heart just wasn't in it. 40 + 41 + I have my hopes up with AT Protocol, however. AppViews remove the need for 42 + implementing display logic for each "post type" manually, and Lexicons allow 43 + for myriad post types contributed by different services. It's an easier place 44 + to live for a lurker such as myself. 45 + 46 + Maybe at some point, I'll take a stab at implementing my own PDS, but until 47 + then--here's the thing I already built.
+51
config/.air.toml
··· 1 + root = "." 2 + testdata_dir = "testdata" 3 + tmp_dir = "tmp" 4 + 5 + [build] 6 + args_bin = [] 7 + bin = "./tmp/main" 8 + cmd = "go build -o ./tmp/main ." 9 + delay = 1000 10 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] 11 + exclude_file = [] 12 + exclude_regex = ["_test.go"] 13 + exclude_unchanged = false 14 + follow_symlink = false 15 + full_bin = "" 16 + include_dir = ["html"] 17 + include_ext = ["go", "tpl", "tmpl", "html"] 18 + include_file = [] 19 + kill_delay = "0s" 20 + log = "build-errors.log" 21 + poll = false 22 + poll_interval = 0 23 + post_cmd = [] 24 + pre_cmd = [] 25 + rerun = false 26 + rerun_delay = 500 27 + send_interrupt = false 28 + stop_on_error = false 29 + 30 + [color] 31 + app = "" 32 + build = "yellow" 33 + main = "magenta" 34 + runner = "green" 35 + watcher = "cyan" 36 + 37 + [log] 38 + main_only = false 39 + time = false 40 + 41 + [misc] 42 + clean_on_exit = false 43 + 44 + [proxy] 45 + app_port = 0 46 + enabled = false 47 + proxy_port = 0 48 + 49 + [screen] 50 + clear_on_rebuild = false 51 + keep_scroll = true
+38
config/Dockerfile
··· 1 + # Build styles 2 + FROM denoland/deno:2.5.2 AS build-styles 3 + WORKDIR /app 4 + 5 + COPY . /app 6 + 7 + RUN deno run --allow-all npm:tailwindcss@3.4.17 -i config/main.css -o static/styles.css -c config/tailwind.config.ts --minify 8 + 9 + FROM golang:1.24-alpine AS build-server 10 + WORKDIR /app 11 + 12 + COPY . /app 13 + 14 + RUN go mod download 15 + 16 + ENV CGO_ENABLED=0 17 + ENV GOOS=linux 18 + RUN go build -ldflags '-s -w -extldflags "-static"' -tags osusergo,netgo,sqlite_omit_load_extension -o /space 19 + 20 + ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz /tmp/litestream.tar.gz 21 + RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz 22 + 23 + FROM alpine 24 + WORKDIR / 25 + 26 + COPY --from=build-styles /app/static /static 27 + COPY --from=build-server /space /space 28 + COPY --from=build-server /usr/local/bin/litestream /usr/local/bin/litestream 29 + 30 + RUN apk add bash 31 + RUN mkdir -p /data 32 + 33 + EXPOSE 80 34 + 35 + COPY config/litestream.yml /etc/litestream.yml 36 + COPY scripts/run.sh /scripts/run.sh 37 + 38 + CMD [ "/scripts/run.sh" ]
+30
config/fly.toml
··· 1 + # fly.toml app configuration file generated for puregarlicspace on 2024-08-06T08:36:09-07:00 2 + # 3 + # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 + # 5 + 6 + app = 'puregarlicspace' 7 + primary_region = 'sjc' 8 + 9 + [build] 10 + dockerfile = "Dockerfile" 11 + 12 + [http_service] 13 + internal_port = 80 14 + force_https = true 15 + auto_stop_machines = 'suspend' 16 + auto_start_machines = true 17 + min_machines_running = 0 18 + processes = ['app'] 19 + 20 + [[vm]] 21 + memory = '1gb' 22 + cpu_kind = 'shared' 23 + cpus = 1 24 + 25 + [mounts] 26 + source = "puregarlicspace_data" 27 + destination = "/data" 28 + initial_size = "1GB" 29 + auto_extend_size_threshold = 80 30 + auto_extend_size_increment = "1GB"
+8
config/litestream.yml
··· 1 + dbs: 2 + - path: /data/data.db 3 + replicas: 4 + - type: s3 5 + bucket: ${AWS_S3_BUCKET_NAME} 6 + endpoint: ${AWS_S3_ENDPOINT} 7 + path: db 8 + force-path-style: true
+10
config/main.css
··· 1 + @tailwind base; 2 + @tailwind components; 3 + @tailwind utilities; 4 + 5 + @layer base { 6 + code { 7 + font-variation-settings: "MONO" 1; 8 + min-width: 0; 9 + } 10 + }
+33
config/tailwind.config.ts
··· 1 + import { Config } from "npm:tailwindcss"; 2 + 3 + export default { 4 + content: [ 5 + "./html/**/*.templ", 6 + ], 7 + theme: { 8 + extend: { 9 + fontFamily: { 10 + sans: "Recursive, sans-serif", 11 + mono: "Recursive, monospace", 12 + }, 13 + colors: { 14 + base: "#191724", 15 + surface: "#1f1d2e", 16 + overlay: "#26233a", 17 + muted: "#6e6a86", 18 + subtle: "#908caa", 19 + text: "#e0def4", 20 + love: "#eb6f92", 21 + gold: "#f6c177", 22 + rose: "#ebbcba", 23 + pine: "#31748f", 24 + foam: "#9ccfd8", 25 + iris: "#c4a7e7", 26 + highlightLow: "#21202e", 27 + highlightMed: "#403d52", 28 + highlightHigh: "#524f67", 29 + }, 30 + }, 31 + }, 32 + plugins: [], 33 + } as Config;
-75
db/storage.go
··· 1 - package db 2 - 3 - import ( 4 - "log" 5 - "time" 6 - 7 - "github.com/jellydator/ttlcache/v3" 8 - "github.com/ostafen/clover/v2" 9 - "go.hacdias.com/indielib/indieauth" 10 - ) 11 - 12 - type Storage struct { 13 - Docs *clover.DB 14 - Authorization *ttlcache.Cache[string, *indieauth.AuthenticationRequest] 15 - } 16 - 17 - type CollectionName string 18 - 19 - var ( 20 - PostCollection CollectionName = "posts" 21 - ) 22 - 23 - func NewStorage() *Storage { 24 - c, err := clover.Open("data/docs") 25 - if err != nil { 26 - log.Fatal(err) 27 - } 28 - 29 - cache := ttlcache.New[string, *indieauth.AuthenticationRequest]( 30 - ttlcache.WithTTL[string, *indieauth.AuthenticationRequest](10 * time.Minute), 31 - ) 32 - 33 - go cache.Start() 34 - 35 - store := &Storage{ 36 - Docs: c, 37 - Authorization: cache, 38 - } 39 - 40 - store.SetupClover() 41 - 42 - return store 43 - } 44 - 45 - func (db *Storage) SetupClover() { 46 - if ok, err := db.Docs.HasCollection(string(PostCollection)); err != nil { 47 - panic(err) 48 - } else if !ok { 49 - err := db.Docs.CreateCollection(string(PostCollection)) 50 - if err != nil { 51 - panic(err) 52 - } 53 - } 54 - 55 - if ok, err := db.Docs.HasIndex(string(PostCollection), "createdAt"); err != nil { 56 - panic(err) 57 - } else if !ok { 58 - err := db.Docs.CreateIndex(string(PostCollection), "createdAt") 59 - if err != nil { 60 - panic(err) 61 - } 62 - } 63 - } 64 - 65 - func (db *Storage) Cleanup() { 66 - if err := db.Docs.Close(); err != nil { 67 - panic(err) 68 - } 69 - 70 - db.Authorization.Stop() 71 - } 72 - 73 - func (db *Storage) SetupTokenCache() { 74 - 75 - }
-4
gen.go
··· 1 - package main 2 - 3 - //go:generate templ generate 4 - //go:generate pnpm tailwindcss -o static/styles.css --minify
+40 -20
go.mod
··· 3 3 go 1.22.5 4 4 5 5 require ( 6 + codeberg.org/gruf/go-ulid v1.1.0 6 7 github.com/aidarkhanov/nanoid v1.0.8 8 + github.com/alecthomas/chroma/v2 v2.14.0 9 + github.com/aws/aws-sdk-go-v2 v1.30.3 10 + github.com/glebarez/sqlite v1.11.0 11 + github.com/go-chi/cors v1.2.1 12 + github.com/h2non/filetype v1.1.3 7 13 github.com/jellydator/ttlcache/v3 v3.2.0 8 14 github.com/joho/godotenv v1.5.1 9 - github.com/ostafen/clover/v2 v2.0.0-alpha.3 10 15 go.hacdias.com/indielib v0.3.1 16 + gorm.io/datatypes v1.2.1 17 + gorm.io/gorm v1.25.11 11 18 ) 12 19 13 20 require ( 14 - github.com/cespare/xxhash v1.1.0 // indirect 21 + filippo.io/edwards25519 v1.1.0 // indirect 22 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect 23 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect 24 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect 25 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect 26 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect 27 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 28 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect 29 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect 30 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect 31 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect 32 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect 33 + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect 34 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect 35 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect 36 + github.com/aws/smithy-go v1.20.3 // indirect 15 37 github.com/cespare/xxhash/v2 v2.3.0 // indirect 16 - github.com/dgraph-io/badger/v3 v3.2103.2 // indirect 17 - github.com/dgraph-io/ristretto v0.1.0 // indirect 18 - github.com/dustin/go-humanize v1.0.0 // indirect 19 - github.com/gogo/protobuf v1.3.2 // indirect 20 - github.com/golang/glog v1.0.0 // indirect 21 - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 22 - github.com/golang/protobuf v1.5.2 // indirect 23 - github.com/golang/snappy v0.0.4 // indirect 24 - github.com/google/flatbuffers v2.0.6+incompatible // indirect 25 - github.com/google/orderedcode v0.0.1 // indirect 26 - github.com/klauspost/compress v1.15.9 // indirect 27 - github.com/pkg/errors v0.9.1 // indirect 28 - github.com/satori/go.uuid v1.2.0 // indirect 29 - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 30 - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 31 - go.etcd.io/bbolt v1.3.6 // indirect 32 - go.opencensus.io v0.23.0 // indirect 38 + github.com/dlclark/regexp2 v1.11.0 // indirect 39 + github.com/dustin/go-humanize v1.0.1 // indirect 40 + github.com/glebarez/go-sqlite v1.21.2 // indirect 41 + github.com/go-sql-driver/mysql v1.8.1 // indirect 42 + github.com/google/uuid v1.3.0 // indirect 43 + github.com/jinzhu/inflection v1.0.0 // indirect 44 + github.com/jinzhu/now v1.1.5 // indirect 45 + github.com/mattn/go-isatty v0.0.20 // indirect 46 + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 33 47 golang.org/x/sync v0.7.0 // indirect 34 48 golang.org/x/sys v0.22.0 // indirect 35 49 golang.org/x/text v0.16.0 // indirect 36 - google.golang.org/protobuf v1.28.1 // indirect 50 + gorm.io/driver/mysql v1.5.6 // indirect 51 + modernc.org/libc v1.22.5 // indirect 52 + modernc.org/mathutil v1.5.0 // indirect 53 + modernc.org/memory v1.5.0 // indirect 54 + modernc.org/sqlite v1.23.1 // indirect 37 55 ) 38 56 39 57 require ( 40 58 github.com/a-h/templ v0.2.747 59 + github.com/aws/aws-sdk-go-v2/config v1.27.27 60 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 41 61 github.com/go-chi/chi/v5 v5.1.0 42 62 github.com/go-chi/httprate v0.12.0 43 63 github.com/golang-jwt/jwt/v5 v5.2.1
+116 -208
go.sum
··· 1 - cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 - github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 - github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= 4 - github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 1 + codeberg.org/gruf/go-ulid v1.1.0 h1:run+0r3hQqF1G7Am1oAt/msoK+FPKnlcQ6sc/biXWqw= 2 + codeberg.org/gruf/go-ulid v1.1.0/go.mod h1:dVNsZJpVTge8+jfBTjAMpnscYSiCqk+g32ZgtorCAMs= 3 + filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 4 + filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 5 5 github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg= 6 6 github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4= 7 7 github.com/aidarkhanov/nanoid v1.0.8 h1:yxyJkgsEDFXP7+97vc6JevMcjyb03Zw+/9fqhlVXBXA= 8 8 github.com/aidarkhanov/nanoid v1.0.8/go.mod h1:vadfZHT+m4uDhttg0yY4wW3GKtl2T6i4d2Age+45pYk= 9 + github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= 10 + github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 11 + github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= 12 + github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= 13 + github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 14 + github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 9 15 github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 10 - github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 11 - github.com/brianvoe/gofakeit/v6 v6.17.0 h1:obbQTJeHfktJtiZzq0Q1bEpsNUs+yHrYlPVWt7BtmJ4= 12 - github.com/brianvoe/gofakeit/v6 v6.17.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= 13 - github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 14 - github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 15 - github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 16 - github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 - github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 16 + github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= 17 + github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= 18 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= 19 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= 20 + github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= 21 + github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= 22 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= 23 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= 24 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= 25 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= 26 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= 27 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= 28 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= 29 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= 30 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 31 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 32 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= 33 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= 34 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= 35 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= 36 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= 37 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= 38 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= 39 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= 40 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= 41 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= 42 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8= 43 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE= 44 + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= 45 + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= 46 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= 47 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= 48 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= 49 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= 50 + github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= 51 + github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 18 52 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 19 53 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 20 - github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 21 - github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 22 - github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 23 - github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 24 - github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 25 - github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 26 - github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 54 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 28 55 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 - github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= 30 - github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= 31 - github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= 32 - github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= 33 - github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= 34 - github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 35 - github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 36 - github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 37 - github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 38 - github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 39 - github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 40 - github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 41 - github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 56 + github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= 57 + github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 58 + github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 59 + github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 60 + github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= 61 + github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= 62 + github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= 63 + github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 42 64 github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= 43 65 github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 66 + github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 67 + github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 44 68 github.com/go-chi/httprate v0.12.0 h1:08D/te3pOTJe5+VAZTQrHxwdsH2NyliiUoRD1naKaMg= 45 69 github.com/go-chi/httprate v0.12.0/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= 46 - github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 47 - github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 70 + github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 71 + github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 72 + github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 48 73 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 49 74 github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 50 - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 51 - github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= 52 - github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= 53 - github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 54 - github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 55 - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 56 - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 57 - github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 58 - github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 59 - github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 60 - github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 61 - github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 62 - github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 63 - github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 64 - github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 65 - github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 66 - github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 67 - github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 68 - github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 69 - github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 70 - github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 71 - github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 72 - github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 73 - github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 74 - github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 75 - github.com/google/flatbuffers v2.0.6+incompatible h1:XHFReMv7nFFusa+CEokzWbzaYocKXI6C7hdU5Kgh9Lw= 76 - github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 77 - github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 78 - github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 79 - github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 80 - github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 81 - github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 82 - github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 83 - github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 84 - github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 75 + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 76 + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 77 + github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 78 + github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 85 79 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 86 80 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 87 81 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 88 - github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= 89 - github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= 90 - github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 91 - github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 92 - github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 82 + github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 83 + github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 84 + github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 85 + github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 86 + github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= 87 + github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= 88 + github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 89 + github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 90 + github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 91 + github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 92 + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= 93 + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 94 + github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= 95 + github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 96 + github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 97 + github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 93 98 github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE= 94 99 github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= 100 + github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 101 + github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 102 + github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 103 + github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 95 104 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 96 105 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 97 - github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 98 - github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 99 - github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 100 - github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= 101 - github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 102 - github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 103 - github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 104 - github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 105 - github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 106 - github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 107 - github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 108 - github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 109 - github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 110 - github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 111 - github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 112 - github.com/ostafen/clover/v2 v2.0.0-alpha.3 h1:fXC7tVHQkUPFlxlj/kD98h0ngrTpIeJymaxVIqDzw3Q= 113 - github.com/ostafen/clover/v2 v2.0.0-alpha.3/go.mod h1:5YCDt+wJDUNN1uSXE5csxSQBuJrNjidkOkJTXWuNhDY= 114 - github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 115 - github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 116 - github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 106 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 107 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 108 + github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 109 + github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 110 + github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= 111 + github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= 117 112 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 118 113 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 119 - github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 120 - github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 114 + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 115 + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 116 + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 121 117 github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= 122 118 github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 123 - github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 124 - github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 125 - github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 126 - github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 127 - github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 128 - github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 129 - github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 130 - github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 131 - github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 132 - github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 133 - github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 134 - github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 135 119 github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 136 120 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 137 - github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 138 - github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 139 - github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 140 - github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 141 121 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 142 122 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 143 - github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 144 - github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= 145 - github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 146 - github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 147 - github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 148 123 github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= 149 - github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 150 - github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 151 - github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 152 - go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= 153 - go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= 154 124 go.hacdias.com/indielib v0.3.1 h1:t6IUp2lfQBa3baXcBN9S/A4fBq4vzMWVbfCQgPcDpy8= 155 125 go.hacdias.com/indielib v0.3.1/go.mod h1:ushJ07W6LxAbZWhyqXzQQxXkalPkZo6cGz5Uj2wOdb4= 156 - go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 157 - go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= 158 - go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 159 126 go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 160 127 go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 161 - golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 162 - golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 163 - golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 164 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 165 - golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 166 - golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 167 - golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 168 - golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 169 - golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 170 - golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 171 - golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 172 - golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 173 - golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 174 - golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 175 - golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 176 - golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 177 - golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 178 - golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 179 - golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 128 + golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 129 + golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 180 130 golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 181 131 golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 182 - golang.org/x/net v0.0.0-20220728211354-c7608f3a8462/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 183 132 golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 184 133 golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 185 - golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 186 134 golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= 187 135 golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 188 - golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 189 - golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 190 - golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 191 - golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 192 - golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 193 - golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 194 136 golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 195 137 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 196 - golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 197 - golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 198 - golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 199 - golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 200 - golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 201 - golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 - golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 138 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 - golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 139 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 207 - golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 208 141 golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 209 142 golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 210 143 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 211 - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 212 - golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 213 - golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 214 144 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 215 - golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 216 145 golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 217 146 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 218 147 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 219 - golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 220 - golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 221 - golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 222 - golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 223 - golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 224 - golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 225 - golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 226 - golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 227 - golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 228 148 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 229 - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 230 - google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 231 - google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 232 - google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 233 - google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 234 - google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 235 - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 236 - google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 237 - google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 238 - google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 239 - google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 240 - google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 241 - google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 242 - google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 243 - google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 244 - google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 245 - google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 246 - google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 247 - google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 248 - google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 249 - google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 250 - google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 251 - google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 252 - google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 253 - google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 254 - google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 255 - gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 256 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 257 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 258 - gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 259 - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 260 149 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 261 150 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 262 - honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 263 - honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 151 + gorm.io/datatypes v1.2.1 h1:r+g0bk4LPCW2v4+Ls7aeNgGme7JYdNDQ2VtvlNUfBh0= 152 + gorm.io/datatypes v1.2.1/go.mod h1:hYK6OTb/1x+m96PgoZZq10UXJ6RvEBb9kRDQ2yyhzGs= 153 + gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= 154 + gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 155 + gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= 156 + gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= 157 + gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= 158 + gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= 159 + gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= 160 + gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= 161 + gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 162 + gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= 163 + gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 164 + modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= 165 + modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= 166 + modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 167 + modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 168 + modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= 169 + modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 170 + modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= 171 + modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= 264 172 willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e h1:TRIOwo0NxN4KVSgYlYmiQktd9I96YgZ3942/JVzhwTM= 265 173 willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e/go.mod h1:zzo0hFA/E/nl1ZAjXiXA7KCKwCTdgBU+7HXltGgHeGA= 266 174 willnorris.com/go/webmention v0.0.0-20220108183051-4a23794272f0 h1:V5+O+YZHchEwu6ZmPcqT1dQ+mHgE356Q+w9SVOQ+QZg=
+35
handlers/media.go
··· 1 + package handlers 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "net/http" 7 + "os" 8 + "strings" 9 + 10 + "github.com/aws/aws-sdk-go-v2/aws" 11 + "github.com/aws/aws-sdk-go-v2/service/s3" 12 + "github.com/puregarlic/space/storage" 13 + ) 14 + 15 + func ServeMedia(w http.ResponseWriter, r *http.Request) { 16 + key := strings.TrimPrefix(r.URL.Path, "/") 17 + 18 + res, err := storage.S3().GetObject(r.Context(), &s3.GetObjectInput{ 19 + Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), 20 + Key: &key, 21 + }) 22 + if err != nil { 23 + fmt.Println("failed to get object", err) 24 + panic(err) 25 + } 26 + 27 + defer res.Body.Close() 28 + 29 + w.Header().Set("Cache-Control", "604800") 30 + 31 + if _, err := io.Copy(w, res.Body); err != nil { 32 + fmt.Println("failed to send object", err) 33 + panic(err) 34 + } 35 + }
+36
handlers/routes.go
··· 1 + package handlers 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/go-chi/chi/v5" 7 + "github.com/puregarlic/space/models" 8 + "github.com/puregarlic/space/storage" 9 + 10 + "github.com/puregarlic/space/html/layouts" 11 + "github.com/puregarlic/space/html/pages" 12 + ) 13 + 14 + func ServeHomePage(w http.ResponseWriter, r *http.Request) { 15 + posts := make([]*models.Post, 0) 16 + result := storage.GORM().Limit(10).Order("created_at DESC").Find(&posts) 17 + if result.Error != nil { 18 + panic(result.Error) 19 + } 20 + 21 + layouts.RenderWithSidebar("", pages.Home(posts)).ServeHTTP(w, r) 22 + } 23 + 24 + func ServePostPage(w http.ResponseWriter, r *http.Request) { 25 + id := chi.URLParam(r, "slug") 26 + post := &models.Post{} 27 + 28 + result := storage.GORM().First(post, "id = ?", id) 29 + 30 + if result.RowsAffected == 0 { 31 + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 32 + return 33 + } 34 + 35 + layouts.RenderWithSidebar(string(post.MicroformatType), pages.Post(post)).ServeHTTP(w, r) 36 + }
+74
handlers/services.go
··· 1 + package handlers 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/puregarlic/space/services" 7 + "github.com/puregarlic/space/storage" 8 + 9 + "github.com/go-chi/chi/v5" 10 + "github.com/go-chi/cors" 11 + "go.hacdias.com/indielib/indieauth" 12 + "go.hacdias.com/indielib/micropub" 13 + ) 14 + 15 + func AttachIndieAuth(r chi.Router, path string, profileUrl string) { 16 + svc := &services.IndieAuth{ 17 + ProfileURL: profileUrl, 18 + Server: indieauth.NewServer(true, nil), 19 + } 20 + 21 + r.Route(path, func(r chi.Router) { 22 + r.Post("/", svc.HandleAuthPOST) 23 + r.Post("/token", svc.HandleToken) 24 + r.Post("/accept", svc.HandleAuthApproval) 25 + 26 + // User authentication portal 27 + r.With(services.MustBasicAuth).Get("/", svc.HandleAuthGET) 28 + }) 29 + 30 + storage.AddRel("authorization_endpoint", path) 31 + storage.AddRel("token_endpoint", path+"/token") 32 + } 33 + 34 + func AttachMicropub(r chi.Router, path string, profileURL string) { 35 + mp := &services.Micropub{ 36 + ProfileURL: profileURL, 37 + } 38 + 39 + mpHandler := micropub.NewHandler( 40 + mp, 41 + micropub.WithMediaEndpoint(profileURL+"micropub/media"), 42 + ) 43 + 44 + r.Route(path, func(r chi.Router) { 45 + // Enable CORS for browser-based clients 46 + r.Use(cors.Handler( 47 + cors.Options{ 48 + AllowedHeaders: []string{ 49 + "Accept", 50 + "Authorization", 51 + "Content-Type", 52 + }, 53 + }, 54 + )) 55 + 56 + // Require access tokens for all Micropub routes 57 + r.Use(services.MustAuth) 58 + 59 + r.Get("/", mpHandler.ServeHTTP) 60 + r.Post("/", mpHandler.ServeHTTP) 61 + r.Post("/media", micropub.NewMediaHandler( 62 + mp.HandleMediaUpload, 63 + func(r *http.Request, scope string) bool { 64 + // IndieKit checks for a `media` scope, not commonly requested 65 + hasMediaScope := mp.HasScope(r, scope) 66 + hasCreateScope := mp.HasScope(r, "create") 67 + 68 + return hasMediaScope || hasCreateScope 69 + }, 70 + ).ServeHTTP) 71 + }) 72 + 73 + storage.AddRel("micropub", path) 74 + }
+21
html/components/head.templ
··· 1 + package components 2 + 3 + import "github.com/puregarlic/space/storage" 4 + 5 + templ Head(title string) { 6 + <head> 7 + if len(title) > 0 { 8 + <title>{ title } | puregarlic dot space</title> 9 + } else { 10 + <title>puregarlic dot space</title> 11 + } 12 + <meta name="viewport" content="width=device-width, initial-scale=1"/> 13 + <link rel="preconnect" href="https://fonts.googleapis.com"/> 14 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/> 15 + <link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap" rel="stylesheet"/> 16 + for _, rel := range storage.GetRels() { 17 + <link rel={ rel.Name } href={ rel.HREF }/> 18 + } 19 + <link rel="stylesheet" href="/static/styles.css"/> 20 + </head> 21 + }
+101
html/components/head_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package components 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import "github.com/puregarlic/space/storage" 12 + 13 + func Head(title string) templ.Component { 14 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 15 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 16 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 17 + if !templ_7745c5c3_IsBuffer { 18 + defer func() { 19 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 20 + if templ_7745c5c3_Err == nil { 21 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 22 + } 23 + }() 24 + } 25 + ctx = templ.InitializeContext(ctx) 26 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 27 + if templ_7745c5c3_Var1 == nil { 28 + templ_7745c5c3_Var1 = templ.NopComponent 29 + } 30 + ctx = templ.ClearChildren(ctx) 31 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head>") 32 + if templ_7745c5c3_Err != nil { 33 + return templ_7745c5c3_Err 34 + } 35 + if len(title) > 0 { 36 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<title>") 37 + if templ_7745c5c3_Err != nil { 38 + return templ_7745c5c3_Err 39 + } 40 + var templ_7745c5c3_Var2 string 41 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) 42 + if templ_7745c5c3_Err != nil { 43 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/head.templ`, Line: 8, Col: 17} 44 + } 45 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 46 + if templ_7745c5c3_Err != nil { 47 + return templ_7745c5c3_Err 48 + } 49 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" | puregarlic dot space</title>") 50 + if templ_7745c5c3_Err != nil { 51 + return templ_7745c5c3_Err 52 + } 53 + } else { 54 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<title>puregarlic dot space</title>") 55 + if templ_7745c5c3_Err != nil { 56 + return templ_7745c5c3_Err 57 + } 58 + } 59 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin><link href=\"https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&amp;display=swap\" rel=\"stylesheet\">") 60 + if templ_7745c5c3_Err != nil { 61 + return templ_7745c5c3_Err 62 + } 63 + for _, rel := range storage.GetRels() { 64 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<link rel=\"") 65 + if templ_7745c5c3_Err != nil { 66 + return templ_7745c5c3_Err 67 + } 68 + var templ_7745c5c3_Var3 string 69 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(rel.Name) 70 + if templ_7745c5c3_Err != nil { 71 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/head.templ`, Line: 17, Col: 23} 72 + } 73 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 74 + if templ_7745c5c3_Err != nil { 75 + return templ_7745c5c3_Err 76 + } 77 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" href=\"") 78 + if templ_7745c5c3_Err != nil { 79 + return templ_7745c5c3_Err 80 + } 81 + var templ_7745c5c3_Var4 string 82 + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(rel.HREF) 83 + if templ_7745c5c3_Err != nil { 84 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/head.templ`, Line: 17, Col: 41} 85 + } 86 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 87 + if templ_7745c5c3_Err != nil { 88 + return templ_7745c5c3_Err 89 + } 90 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") 91 + if templ_7745c5c3_Err != nil { 92 + return templ_7745c5c3_Err 93 + } 94 + } 95 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<link rel=\"stylesheet\" href=\"/static/styles.css\"></head>") 96 + if templ_7745c5c3_Err != nil { 97 + return templ_7745c5c3_Err 98 + } 99 + return templ_7745c5c3_Err 100 + }) 101 + }
+9
html/components/posts/note.templ
··· 1 + package posts 2 + 3 + import "github.com/puregarlic/space/models" 4 + 5 + templ Note(post *models.Post) { 6 + <div class="bg-surface p-4 first:rounded-t last:rounded-b border-2 border-overlay"> 7 + { GetPostJSONProperty(post, "content")[0] } 8 + </div> 9 + }
+50
html/components/posts/note_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package posts 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import "github.com/puregarlic/space/models" 12 + 13 + func Note(post *models.Post) templ.Component { 14 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 15 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 16 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 17 + if !templ_7745c5c3_IsBuffer { 18 + defer func() { 19 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 20 + if templ_7745c5c3_Err == nil { 21 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 22 + } 23 + }() 24 + } 25 + ctx = templ.InitializeContext(ctx) 26 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 27 + if templ_7745c5c3_Var1 == nil { 28 + templ_7745c5c3_Var1 = templ.NopComponent 29 + } 30 + ctx = templ.ClearChildren(ctx) 31 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-surface p-4 first:rounded-t last:rounded-b border-2 border-overlay\">") 32 + if templ_7745c5c3_Err != nil { 33 + return templ_7745c5c3_Err 34 + } 35 + var templ_7745c5c3_Var2 string 36 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(GetPostJSONProperty(post, "content")[0]) 37 + if templ_7745c5c3_Err != nil { 38 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/note.templ`, Line: 7, Col: 43} 39 + } 40 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 41 + if templ_7745c5c3_Err != nil { 42 + return templ_7745c5c3_Err 43 + } 44 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") 45 + if templ_7745c5c3_Err != nil { 46 + return templ_7745c5c3_Err 47 + } 48 + return templ_7745c5c3_Err 49 + }) 50 + }
+18
html/components/posts/photo.templ
··· 1 + package posts 2 + 3 + import "github.com/puregarlic/space/models" 4 + 5 + templ Photo(post *models.Post) { 6 + <div class="bg-base"> 7 + for index, photo := range GetPostJSONProperty(post, "photo") { 8 + <figure class="relative group last:rounded-b"> 9 + <img class="w-full border-x-2 border-overlay group-first:rounded-t group-first:border-t-2" src={ photo }/> 10 + <figcaption 11 + class="p-4 bg-surface group-last:rounded-b group-last:border-b-2 group-last:border-x-2 group-last:border-overlay" 12 + > 13 + { GetPostJSONProperty(post, "content")[index] } 14 + </figcaption> 15 + </figure> 16 + } 17 + </div> 18 + }
+73
html/components/posts/photo_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package posts 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import "github.com/puregarlic/space/models" 12 + 13 + func Photo(post *models.Post) templ.Component { 14 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 15 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 16 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 17 + if !templ_7745c5c3_IsBuffer { 18 + defer func() { 19 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 20 + if templ_7745c5c3_Err == nil { 21 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 22 + } 23 + }() 24 + } 25 + ctx = templ.InitializeContext(ctx) 26 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 27 + if templ_7745c5c3_Var1 == nil { 28 + templ_7745c5c3_Var1 = templ.NopComponent 29 + } 30 + ctx = templ.ClearChildren(ctx) 31 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-base\">") 32 + if templ_7745c5c3_Err != nil { 33 + return templ_7745c5c3_Err 34 + } 35 + for index, photo := range GetPostJSONProperty(post, "photo") { 36 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<figure class=\"relative group last:rounded-b\"><img class=\"w-full border-x-2 border-overlay group-first:rounded-t group-first:border-t-2\" src=\"") 37 + if templ_7745c5c3_Err != nil { 38 + return templ_7745c5c3_Err 39 + } 40 + var templ_7745c5c3_Var2 string 41 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(photo) 42 + if templ_7745c5c3_Err != nil { 43 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/photo.templ`, Line: 9, Col: 106} 44 + } 45 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 46 + if templ_7745c5c3_Err != nil { 47 + return templ_7745c5c3_Err 48 + } 49 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><figcaption class=\"p-4 bg-surface group-last:rounded-b group-last:border-b-2 group-last:border-x-2 group-last:border-overlay\">") 50 + if templ_7745c5c3_Err != nil { 51 + return templ_7745c5c3_Err 52 + } 53 + var templ_7745c5c3_Var3 string 54 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(GetPostJSONProperty(post, "content")[index]) 55 + if templ_7745c5c3_Err != nil { 56 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/photo.templ`, Line: 13, Col: 50} 57 + } 58 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 59 + if templ_7745c5c3_Err != nil { 60 + return templ_7745c5c3_Err 61 + } 62 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</figcaption></figure>") 63 + if templ_7745c5c3_Err != nil { 64 + return templ_7745c5c3_Err 65 + } 66 + } 67 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") 68 + if templ_7745c5c3_Err != nil { 69 + return templ_7745c5c3_Err 70 + } 71 + return templ_7745c5c3_Err 72 + }) 73 + }
+92
html/components/posts/post.templ
··· 1 + package posts 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + 7 + "github.com/puregarlic/space/models" 8 + "github.com/samber/lo" 9 + "go.hacdias.com/indielib/microformats" 10 + ) 11 + 12 + var ImplementedPostTypes = []microformats.Type{ 13 + microformats.TypeNote, 14 + microformats.TypePhoto, 15 + } 16 + 17 + func GetPostJSONProperty(post *models.Post, name string) []string { 18 + var tmp map[string]any 19 + if err := json.Unmarshal(post.Properties, &tmp); err != nil { 20 + panic(err) 21 + } 22 + 23 + prop, ok := tmp[name] 24 + if !ok { 25 + return []string{""} 26 + } 27 + 28 + var out []string 29 + for _, val := range prop.([]any) { 30 + out = append(out, val.(string)) 31 + } 32 + 33 + return out 34 + } 35 + 36 + func formatPostTypeName(mfType microformats.Type) string { 37 + has := lo.ContainsBy(ImplementedPostTypes, func(postType microformats.Type) bool { 38 + return postType == mfType 39 + }) 40 + 41 + if has { 42 + return string(mfType) 43 + } else { 44 + return fmt.Sprintf("%s (oops!)", string(mfType)) 45 + } 46 + } 47 + 48 + templ PostFeedHeader(post *models.Post) { 49 + <div class="px-3 py-2 bg-surface text-xs text-muted flex items-center justify-between rounded-t border-2 border-b-0 border-overlay"> 50 + <p>{ post.Timestamp() }</p> 51 + <p class="flex gap-1.5"> 52 + { formatPostTypeName(post.MicroformatType) } 53 + <span class="text-muted/40">&#8226;</span> 54 + <a 55 + class="hover:underline hover:text-iris flex items-center gap-1 transition" 56 + target="_blank" 57 + rel="noopener noreferrer" 58 + href={ templ.URL("/posts/" + post.ID.String()) } 59 + > 60 + open 61 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4 -mt-px"> 62 + <path d="M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z"></path> 63 + <path d="M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z"></path> 64 + </svg> 65 + </a> 66 + </p> 67 + </div> 68 + } 69 + 70 + templ PostDetails(post *models.Post) { 71 + <dl class="grid md:grid-cols-2 gap-4"> 72 + <div> 73 + <dt class="mb-1 text-sm text-muted">Posted At</dt> 74 + <dd class="text-subtle">{ post.Timestamp() }</dd> 75 + </div> 76 + <div> 77 + <dt class="mb-1 text-sm text-muted">Post Type</dt> 78 + <dd class="text-subtle">{ string(post.MicroformatType) }</dd> 79 + </div> 80 + </dl> 81 + } 82 + 83 + templ PostContent(post *models.Post) { 84 + switch post.MicroformatType { 85 + case microformats.TypePhoto: 86 + @Photo(post) 87 + case microformats.TypeNote: 88 + @Note(post) 89 + default: 90 + @Unsupported(post) 91 + } 92 + }
+206
html/components/posts/post_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package posts 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import ( 12 + "encoding/json" 13 + "fmt" 14 + 15 + "github.com/puregarlic/space/models" 16 + "github.com/samber/lo" 17 + "go.hacdias.com/indielib/microformats" 18 + ) 19 + 20 + var ImplementedPostTypes = []microformats.Type{ 21 + microformats.TypeNote, 22 + microformats.TypePhoto, 23 + } 24 + 25 + func GetPostJSONProperty(post *models.Post, name string) []string { 26 + var tmp map[string]any 27 + if err := json.Unmarshal(post.Properties, &tmp); err != nil { 28 + panic(err) 29 + } 30 + 31 + prop, ok := tmp[name] 32 + if !ok { 33 + return []string{""} 34 + } 35 + 36 + var out []string 37 + for _, val := range prop.([]any) { 38 + out = append(out, val.(string)) 39 + } 40 + 41 + return out 42 + } 43 + 44 + func formatPostTypeName(mfType microformats.Type) string { 45 + has := lo.ContainsBy(ImplementedPostTypes, func(postType microformats.Type) bool { 46 + return postType == mfType 47 + }) 48 + 49 + if has { 50 + return string(mfType) 51 + } else { 52 + return fmt.Sprintf("%s (oops!)", string(mfType)) 53 + } 54 + } 55 + 56 + func PostFeedHeader(post *models.Post) templ.Component { 57 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 58 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 59 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 60 + if !templ_7745c5c3_IsBuffer { 61 + defer func() { 62 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 63 + if templ_7745c5c3_Err == nil { 64 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 65 + } 66 + }() 67 + } 68 + ctx = templ.InitializeContext(ctx) 69 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 70 + if templ_7745c5c3_Var1 == nil { 71 + templ_7745c5c3_Var1 = templ.NopComponent 72 + } 73 + ctx = templ.ClearChildren(ctx) 74 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-3 py-2 bg-surface text-xs text-muted flex items-center justify-between rounded-t border-2 border-b-0 border-overlay\"><p>") 75 + if templ_7745c5c3_Err != nil { 76 + return templ_7745c5c3_Err 77 + } 78 + var templ_7745c5c3_Var2 string 79 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(post.Timestamp()) 80 + if templ_7745c5c3_Err != nil { 81 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 50, Col: 23} 82 + } 83 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 84 + if templ_7745c5c3_Err != nil { 85 + return templ_7745c5c3_Err 86 + } 87 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"flex gap-1.5\">") 88 + if templ_7745c5c3_Err != nil { 89 + return templ_7745c5c3_Err 90 + } 91 + var templ_7745c5c3_Var3 string 92 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(formatPostTypeName(post.MicroformatType)) 93 + if templ_7745c5c3_Err != nil { 94 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 52, Col: 45} 95 + } 96 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 97 + if templ_7745c5c3_Err != nil { 98 + return templ_7745c5c3_Err 99 + } 100 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <span class=\"text-muted/40\">&#8226;</span> <a class=\"hover:underline hover:text-iris flex items-center gap-1 transition\" target=\"_blank\" rel=\"noopener noreferrer\" href=\"") 101 + if templ_7745c5c3_Err != nil { 102 + return templ_7745c5c3_Err 103 + } 104 + var templ_7745c5c3_Var4 templ.SafeURL = templ.URL("/posts/" + post.ID.String()) 105 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4))) 106 + if templ_7745c5c3_Err != nil { 107 + return templ_7745c5c3_Err 108 + } 109 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">open <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"size-4 -mt-px\"><path d=\"M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z\"></path> <path d=\"M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z\"></path></svg></a></p></div>") 110 + if templ_7745c5c3_Err != nil { 111 + return templ_7745c5c3_Err 112 + } 113 + return templ_7745c5c3_Err 114 + }) 115 + } 116 + 117 + func PostDetails(post *models.Post) templ.Component { 118 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 119 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 120 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 121 + if !templ_7745c5c3_IsBuffer { 122 + defer func() { 123 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 124 + if templ_7745c5c3_Err == nil { 125 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 126 + } 127 + }() 128 + } 129 + ctx = templ.InitializeContext(ctx) 130 + templ_7745c5c3_Var5 := templ.GetChildren(ctx) 131 + if templ_7745c5c3_Var5 == nil { 132 + templ_7745c5c3_Var5 = templ.NopComponent 133 + } 134 + ctx = templ.ClearChildren(ctx) 135 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<dl class=\"grid md:grid-cols-2 gap-4\"><div><dt class=\"mb-1 text-sm text-muted\">Posted At</dt><dd class=\"text-subtle\">") 136 + if templ_7745c5c3_Err != nil { 137 + return templ_7745c5c3_Err 138 + } 139 + var templ_7745c5c3_Var6 string 140 + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(post.Timestamp()) 141 + if templ_7745c5c3_Err != nil { 142 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 74, Col: 45} 143 + } 144 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 145 + if templ_7745c5c3_Err != nil { 146 + return templ_7745c5c3_Err 147 + } 148 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</dd></div><div><dt class=\"mb-1 text-sm text-muted\">Post Type</dt><dd class=\"text-subtle\">") 149 + if templ_7745c5c3_Err != nil { 150 + return templ_7745c5c3_Err 151 + } 152 + var templ_7745c5c3_Var7 string 153 + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(string(post.MicroformatType)) 154 + if templ_7745c5c3_Err != nil { 155 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 78, Col: 57} 156 + } 157 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 158 + if templ_7745c5c3_Err != nil { 159 + return templ_7745c5c3_Err 160 + } 161 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</dd></div></dl>") 162 + if templ_7745c5c3_Err != nil { 163 + return templ_7745c5c3_Err 164 + } 165 + return templ_7745c5c3_Err 166 + }) 167 + } 168 + 169 + func PostContent(post *models.Post) templ.Component { 170 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 171 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 172 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 173 + if !templ_7745c5c3_IsBuffer { 174 + defer func() { 175 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 176 + if templ_7745c5c3_Err == nil { 177 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 178 + } 179 + }() 180 + } 181 + ctx = templ.InitializeContext(ctx) 182 + templ_7745c5c3_Var8 := templ.GetChildren(ctx) 183 + if templ_7745c5c3_Var8 == nil { 184 + templ_7745c5c3_Var8 = templ.NopComponent 185 + } 186 + ctx = templ.ClearChildren(ctx) 187 + switch post.MicroformatType { 188 + case microformats.TypePhoto: 189 + templ_7745c5c3_Err = Photo(post).Render(ctx, templ_7745c5c3_Buffer) 190 + if templ_7745c5c3_Err != nil { 191 + return templ_7745c5c3_Err 192 + } 193 + case microformats.TypeNote: 194 + templ_7745c5c3_Err = Note(post).Render(ctx, templ_7745c5c3_Buffer) 195 + if templ_7745c5c3_Err != nil { 196 + return templ_7745c5c3_Err 197 + } 198 + default: 199 + templ_7745c5c3_Err = Unsupported(post).Render(ctx, templ_7745c5c3_Buffer) 200 + if templ_7745c5c3_Err != nil { 201 + return templ_7745c5c3_Err 202 + } 203 + } 204 + return templ_7745c5c3_Err 205 + }) 206 + }
+55
html/components/posts/unsupported.templ
··· 1 + package posts 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + 7 + "github.com/alecthomas/chroma/v2/formatters/html" 8 + "github.com/alecthomas/chroma/v2/lexers" 9 + "github.com/alecthomas/chroma/v2/styles" 10 + "github.com/puregarlic/space/models" 11 + ) 12 + 13 + var style = styles.Get("rose-pine") 14 + var lexer = lexers.Get("json") 15 + var formatter = html.New(html.TabWidth(2), html.WithClasses(true)) 16 + 17 + var dedupeSyntaxStyles = templ.NewOnceHandle() 18 + 19 + func renderPostAsJSON(post *models.Post) string { 20 + contents, err := json.MarshalIndent(post.Properties, "", " ") 21 + if err != nil { 22 + panic(err) 23 + } 24 + 25 + iterator, err := lexer.Tokenise(nil, string(contents)) 26 + 27 + var buf bytes.Buffer 28 + formatter.Format(&buf, style, iterator) 29 + 30 + return buf.String() 31 + } 32 + 33 + func generateSyntaxClassNames() string { 34 + var buf bytes.Buffer 35 + if err := formatter.WriteCSS(&buf, style); err != nil { 36 + panic(err) 37 + } 38 + 39 + return "<style>" + buf.String() + "</style>" 40 + } 41 + 42 + templ syntaxStyleTag() { 43 + @templ.Raw(generateSyntaxClassNames()) 44 + } 45 + 46 + templ Unsupported(post *models.Post) { 47 + <div 48 + class="block p-4 bg-base overflow-x-scroll min-w-0 first:rounded-t last:rounded-b border-2 border-overlay" 49 + > 50 + @dedupeSyntaxStyles.Once() { 51 + @syntaxStyleTag() 52 + } 53 + @templ.Raw(renderPostAsJSON(post)) 54 + </div> 55 + }
+130
html/components/posts/unsupported_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package posts 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import ( 12 + "bytes" 13 + "encoding/json" 14 + 15 + "github.com/alecthomas/chroma/v2/formatters/html" 16 + "github.com/alecthomas/chroma/v2/lexers" 17 + "github.com/alecthomas/chroma/v2/styles" 18 + "github.com/puregarlic/space/models" 19 + ) 20 + 21 + var style = styles.Get("rose-pine") 22 + var lexer = lexers.Get("json") 23 + var formatter = html.New(html.TabWidth(2), html.WithClasses(true)) 24 + 25 + var dedupeSyntaxStyles = templ.NewOnceHandle() 26 + 27 + func renderPostAsJSON(post *models.Post) string { 28 + contents, err := json.MarshalIndent(post.Properties, "", " ") 29 + if err != nil { 30 + panic(err) 31 + } 32 + 33 + iterator, err := lexer.Tokenise(nil, string(contents)) 34 + 35 + var buf bytes.Buffer 36 + formatter.Format(&buf, style, iterator) 37 + 38 + return buf.String() 39 + } 40 + 41 + func generateSyntaxClassNames() string { 42 + var buf bytes.Buffer 43 + if err := formatter.WriteCSS(&buf, style); err != nil { 44 + panic(err) 45 + } 46 + 47 + return "<style>" + buf.String() + "</style>" 48 + } 49 + 50 + func syntaxStyleTag() templ.Component { 51 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 52 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 53 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 54 + if !templ_7745c5c3_IsBuffer { 55 + defer func() { 56 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 57 + if templ_7745c5c3_Err == nil { 58 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 59 + } 60 + }() 61 + } 62 + ctx = templ.InitializeContext(ctx) 63 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 64 + if templ_7745c5c3_Var1 == nil { 65 + templ_7745c5c3_Var1 = templ.NopComponent 66 + } 67 + ctx = templ.ClearChildren(ctx) 68 + templ_7745c5c3_Err = templ.Raw(generateSyntaxClassNames()).Render(ctx, templ_7745c5c3_Buffer) 69 + if templ_7745c5c3_Err != nil { 70 + return templ_7745c5c3_Err 71 + } 72 + return templ_7745c5c3_Err 73 + }) 74 + } 75 + 76 + func Unsupported(post *models.Post) templ.Component { 77 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 78 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 79 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 80 + if !templ_7745c5c3_IsBuffer { 81 + defer func() { 82 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 83 + if templ_7745c5c3_Err == nil { 84 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 85 + } 86 + }() 87 + } 88 + ctx = templ.InitializeContext(ctx) 89 + templ_7745c5c3_Var2 := templ.GetChildren(ctx) 90 + if templ_7745c5c3_Var2 == nil { 91 + templ_7745c5c3_Var2 = templ.NopComponent 92 + } 93 + ctx = templ.ClearChildren(ctx) 94 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block p-4 bg-base overflow-x-scroll min-w-0 first:rounded-t last:rounded-b border-2 border-overlay\">") 95 + if templ_7745c5c3_Err != nil { 96 + return templ_7745c5c3_Err 97 + } 98 + templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 99 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 100 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 101 + if !templ_7745c5c3_IsBuffer { 102 + defer func() { 103 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 104 + if templ_7745c5c3_Err == nil { 105 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 106 + } 107 + }() 108 + } 109 + ctx = templ.InitializeContext(ctx) 110 + templ_7745c5c3_Err = syntaxStyleTag().Render(ctx, templ_7745c5c3_Buffer) 111 + if templ_7745c5c3_Err != nil { 112 + return templ_7745c5c3_Err 113 + } 114 + return templ_7745c5c3_Err 115 + }) 116 + templ_7745c5c3_Err = dedupeSyntaxStyles.Once().Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer) 117 + if templ_7745c5c3_Err != nil { 118 + return templ_7745c5c3_Err 119 + } 120 + templ_7745c5c3_Err = templ.Raw(renderPostAsJSON(post)).Render(ctx, templ_7745c5c3_Buffer) 121 + if templ_7745c5c3_Err != nil { 122 + return templ_7745c5c3_Err 123 + } 124 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") 125 + if templ_7745c5c3_Err != nil { 126 + return templ_7745c5c3_Err 127 + } 128 + return templ_7745c5c3_Err 129 + }) 130 + }
+12
html/components/sidebar.templ
··· 1 + package components 2 + 3 + templ Sidebar() { 4 + <aside class="min-w-0"> 5 + <div class="w-full md:sticky md:top-8"> 6 + <h1 class="font-extrabold text-xl">puregarlic dot space</h1> 7 + <p class="font-light mt-3 text-subtle italic md:text-sm"> 8 + this space is mine, it was <a class="underline hover:text-iris" href="https://github.com/puregarlic/space">made by me</a>! 9 + </p> 10 + </div> 11 + </aside> 12 + }
+35
html/components/sidebar_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package components 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + func Sidebar() templ.Component { 12 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 13 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 14 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 15 + if !templ_7745c5c3_IsBuffer { 16 + defer func() { 17 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 18 + if templ_7745c5c3_Err == nil { 19 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 20 + } 21 + }() 22 + } 23 + ctx = templ.InitializeContext(ctx) 24 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 25 + if templ_7745c5c3_Var1 == nil { 26 + templ_7745c5c3_Var1 = templ.NopComponent 27 + } 28 + ctx = templ.ClearChildren(ctx) 29 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<aside class=\"min-w-0\"><div class=\"w-full md:sticky md:top-8\"><h1 class=\"font-extrabold text-xl\">puregarlic dot space</h1><p class=\"font-light mt-3 text-subtle italic md:text-sm\">this space is mine, it was <a class=\"underline hover:text-iris\" href=\"https://github.com/puregarlic/space\">made by me</a>!</p></div></aside>") 30 + if templ_7745c5c3_Err != nil { 31 + return templ_7745c5c3_Err 32 + } 33 + return templ_7745c5c3_Err 34 + }) 35 + }
+22
html/layouts/default.templ
··· 1 + package layouts 2 + 3 + import ( 4 + "github.com/puregarlic/space/html/components" 5 + "net/http" 6 + ) 7 + 8 + func RenderDefault(title string, page templ.Component) http.Handler { 9 + document := Default(title, page) 10 + 11 + return templ.Handler(document) 12 + } 13 + 14 + templ Default(title string, body templ.Component) { 15 + <!DOCTYPE html> 16 + <html> 17 + @components.Head(title) 18 + <body class="px-4 py-12 md:py-20 text-text bg-base"> 19 + @body 20 + </body> 21 + </html> 22 + }
+62
html/layouts/default_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package layouts 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import ( 12 + "github.com/puregarlic/space/html/components" 13 + "net/http" 14 + ) 15 + 16 + func RenderDefault(title string, page templ.Component) http.Handler { 17 + document := Default(title, page) 18 + 19 + return templ.Handler(document) 20 + } 21 + 22 + func Default(title string, body templ.Component) templ.Component { 23 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 24 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 25 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 26 + if !templ_7745c5c3_IsBuffer { 27 + defer func() { 28 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 29 + if templ_7745c5c3_Err == nil { 30 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 31 + } 32 + }() 33 + } 34 + ctx = templ.InitializeContext(ctx) 35 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 36 + if templ_7745c5c3_Var1 == nil { 37 + templ_7745c5c3_Var1 = templ.NopComponent 38 + } 39 + ctx = templ.ClearChildren(ctx) 40 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html>") 41 + if templ_7745c5c3_Err != nil { 42 + return templ_7745c5c3_Err 43 + } 44 + templ_7745c5c3_Err = components.Head(title).Render(ctx, templ_7745c5c3_Buffer) 45 + if templ_7745c5c3_Err != nil { 46 + return templ_7745c5c3_Err 47 + } 48 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body class=\"px-4 py-12 md:py-20 text-text bg-base\">") 49 + if templ_7745c5c3_Err != nil { 50 + return templ_7745c5c3_Err 51 + } 52 + templ_7745c5c3_Err = body.Render(ctx, templ_7745c5c3_Buffer) 53 + if templ_7745c5c3_Err != nil { 54 + return templ_7745c5c3_Err 55 + } 56 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>") 57 + if templ_7745c5c3_Err != nil { 58 + return templ_7745c5c3_Err 59 + } 60 + return templ_7745c5c3_Err 61 + }) 62 + }
+29
html/layouts/sidebar.templ
··· 1 + package layouts 2 + 3 + import ( 4 + "github.com/puregarlic/space/html/components" 5 + "net/http" 6 + ) 7 + 8 + func RenderWithSidebar(title string, body templ.Component) http.Handler { 9 + page := WithSidebar(title, body) 10 + 11 + return templ.Handler(page) 12 + } 13 + 14 + templ WithSidebar(title string, body templ.Component) { 15 + <!DOCTYPE html> 16 + <html> 17 + @components.Head(title) 18 + <body class="px-4 py-12 md:py-20 text-text bg-base"> 19 + <div class="mx-auto max-w-screen-lg grid md:grid-cols-[1fr_2fr] gap-12 gap-16"> 20 + <aside class="min-w-0"> 21 + @components.Sidebar() 22 + </aside> 23 + <main class="min-w-0"> 24 + @body 25 + </main> 26 + </div> 27 + </body> 28 + </html> 29 + }
+70
html/layouts/sidebar_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package layouts 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import ( 12 + "github.com/puregarlic/space/html/components" 13 + "net/http" 14 + ) 15 + 16 + func RenderWithSidebar(title string, body templ.Component) http.Handler { 17 + page := WithSidebar(title, body) 18 + 19 + return templ.Handler(page) 20 + } 21 + 22 + func WithSidebar(title string, body templ.Component) templ.Component { 23 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 24 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 25 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 26 + if !templ_7745c5c3_IsBuffer { 27 + defer func() { 28 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 29 + if templ_7745c5c3_Err == nil { 30 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 31 + } 32 + }() 33 + } 34 + ctx = templ.InitializeContext(ctx) 35 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 36 + if templ_7745c5c3_Var1 == nil { 37 + templ_7745c5c3_Var1 = templ.NopComponent 38 + } 39 + ctx = templ.ClearChildren(ctx) 40 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html>") 41 + if templ_7745c5c3_Err != nil { 42 + return templ_7745c5c3_Err 43 + } 44 + templ_7745c5c3_Err = components.Head(title).Render(ctx, templ_7745c5c3_Buffer) 45 + if templ_7745c5c3_Err != nil { 46 + return templ_7745c5c3_Err 47 + } 48 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body class=\"px-4 py-12 md:py-20 text-text bg-base\"><div class=\"mx-auto max-w-screen-lg grid md:grid-cols-[1fr_2fr] gap-12 gap-16\"><aside class=\"min-w-0\">") 49 + if templ_7745c5c3_Err != nil { 50 + return templ_7745c5c3_Err 51 + } 52 + templ_7745c5c3_Err = components.Sidebar().Render(ctx, templ_7745c5c3_Buffer) 53 + if templ_7745c5c3_Err != nil { 54 + return templ_7745c5c3_Err 55 + } 56 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</aside><main class=\"min-w-0\">") 57 + if templ_7745c5c3_Err != nil { 58 + return templ_7745c5c3_Err 59 + } 60 + templ_7745c5c3_Err = body.Render(ctx, templ_7745c5c3_Buffer) 61 + if templ_7745c5c3_Err != nil { 62 + return templ_7745c5c3_Err 63 + } 64 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</main></div></body></html>") 65 + if templ_7745c5c3_Err != nil { 66 + return templ_7745c5c3_Err 67 + } 68 + return templ_7745c5c3_Err 69 + }) 70 + }
+63
html/pages/auth.templ
··· 1 + package pages 2 + 3 + import ( 4 + "go.hacdias.com/indielib/indieauth" 5 + "strings" 6 + ) 7 + 8 + templ Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata, nonceId string, nonce string) { 9 + <main class="mx-auto max-w-screen-sm"> 10 + <p class="text-sm font-thin italic">authorize access to</p> 11 + <h1 class="mb-8 text-3xl font-extrabold">puregarlic dot space</h1> 12 + <div class="pt-6 border border-highlightMed rounded bg-surface"> 13 + if app != nil { 14 + <div class="px-6 flex gap-6 items-center"> 15 + if len(app.Logo) > 0 { 16 + <img class="max-w-12" src={ app.Logo }/> 17 + } 18 + <div> 19 + <h2 class="font-bold text-lg">{ app.Name }</h2> 20 + if len(app.Author) > 0 { 21 + <p class="text-sm font-light">by { app.Author }</p> 22 + } 23 + </div> 24 + </div> 25 + } else { 26 + <h2 class="px-6 font-bold text-subtle">unidentified client</h2> 27 + } 28 + <div class="mt-6 grid md:grid-cols-[max-content_1fr] *:border-highlightMed"> 29 + <h3 class="px-6 pt-4 pb-2 md:py-4 border-t bg-overlay md:border-r text-subtle font-bold">Client ID</h3> 30 + <p class="px-6 pb-4 pt-1 md:py-4 md:border-t bg-overlay min-w-0 overflow-x-scroll">{ req.ClientID }</p> 31 + <h3 class="px-6 pt-4 pb-2 md:py-4 border-t md:border-b md:border-r text-subtle font-bold">Redirect URL</h3> 32 + <p class="px-6 pb-4 pt-1 md:py-4 md:border-y min-w-0 overflow-x-scroll">{ req.RedirectURI }</p> 33 + <h3 class="px-6 pt-4 pb-2 md:py-4 border-t bg-overlay md:border-r text-subtle font-bold">Scopes</h3> 34 + <ul class="px-6 pb-4 pt-1 md:py-4 bg-overlay flex flex-wrap gap-3"> 35 + for _, scope := range req.Scopes { 36 + <li class="px-2 py-1 text-sm bg-pine rounded">{ scope }</li> 37 + } 38 + </ul> 39 + </div> 40 + </div> 41 + <form method="post" action="/authorization/accept"> 42 + <input type="hidden" name="response_type" value="code"/> 43 + <input type="hidden" name="scope" value={ strings.Join(req.Scopes, " ") }/> 44 + <input type="hidden" name="redirect_uri" value={ req.RedirectURI }/> 45 + <input type="hidden" name="client_id" value={ req.ClientID }/> 46 + <input type="hidden" name="state" value={ req.State }/> 47 + <input type="hidden" name="code_challenge" value={ req.CodeChallenge }/> 48 + <input type="hidden" name="code_challenge_method" value={ req.CodeChallengeMethod }/> 49 + // CSRF protections 50 + <input type="hidden" name="nonce_id" value={ nonceId }/> 51 + <input type="hidden" name="nonce" value={ nonce }/> 52 + <button 53 + class="mt-8 px-3 py-2 flex items-center justify-center gap-2 bg-surface border border-highlightMed text-sm font-bold transition rounded hover:bg-foam hover:text-surface" 54 + id="submit" 55 + > 56 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4"> 57 + <path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd"></path> 58 + </svg> 59 + Authorize 60 + </button> 61 + </form> 62 + </main> 63 + }
+267
html/pages/auth_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package pages 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import ( 12 + "go.hacdias.com/indielib/indieauth" 13 + "strings" 14 + ) 15 + 16 + func Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata, nonceId string, nonce string) templ.Component { 17 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 18 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 19 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 20 + if !templ_7745c5c3_IsBuffer { 21 + defer func() { 22 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 23 + if templ_7745c5c3_Err == nil { 24 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 25 + } 26 + }() 27 + } 28 + ctx = templ.InitializeContext(ctx) 29 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 30 + if templ_7745c5c3_Var1 == nil { 31 + templ_7745c5c3_Var1 = templ.NopComponent 32 + } 33 + ctx = templ.ClearChildren(ctx) 34 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<main class=\"mx-auto max-w-screen-sm\"><p class=\"text-sm font-thin italic\">authorize access to</p><h1 class=\"mb-8 text-3xl font-extrabold\">puregarlic dot space</h1><div class=\"pt-6 border border-highlightMed rounded bg-surface\">") 35 + if templ_7745c5c3_Err != nil { 36 + return templ_7745c5c3_Err 37 + } 38 + if app != nil { 39 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-6 flex gap-6 items-center\">") 40 + if templ_7745c5c3_Err != nil { 41 + return templ_7745c5c3_Err 42 + } 43 + if len(app.Logo) > 0 { 44 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img class=\"max-w-12\" src=\"") 45 + if templ_7745c5c3_Err != nil { 46 + return templ_7745c5c3_Err 47 + } 48 + var templ_7745c5c3_Var2 string 49 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(app.Logo) 50 + if templ_7745c5c3_Err != nil { 51 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 16, Col: 42} 52 + } 53 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 54 + if templ_7745c5c3_Err != nil { 55 + return templ_7745c5c3_Err 56 + } 57 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") 58 + if templ_7745c5c3_Err != nil { 59 + return templ_7745c5c3_Err 60 + } 61 + } 62 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div><h2 class=\"font-bold text-lg\">") 63 + if templ_7745c5c3_Err != nil { 64 + return templ_7745c5c3_Err 65 + } 66 + var templ_7745c5c3_Var3 string 67 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(app.Name) 68 + if templ_7745c5c3_Err != nil { 69 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 19, Col: 46} 70 + } 71 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 72 + if templ_7745c5c3_Err != nil { 73 + return templ_7745c5c3_Err 74 + } 75 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h2>") 76 + if templ_7745c5c3_Err != nil { 77 + return templ_7745c5c3_Err 78 + } 79 + if len(app.Author) > 0 { 80 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"text-sm font-light\">by ") 81 + if templ_7745c5c3_Err != nil { 82 + return templ_7745c5c3_Err 83 + } 84 + var templ_7745c5c3_Var4 string 85 + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(app.Author) 86 + if templ_7745c5c3_Err != nil { 87 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 21, Col: 52} 88 + } 89 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 90 + if templ_7745c5c3_Err != nil { 91 + return templ_7745c5c3_Err 92 + } 93 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>") 94 + if templ_7745c5c3_Err != nil { 95 + return templ_7745c5c3_Err 96 + } 97 + } 98 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>") 99 + if templ_7745c5c3_Err != nil { 100 + return templ_7745c5c3_Err 101 + } 102 + } else { 103 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h2 class=\"px-6 font-bold text-subtle\">unidentified client</h2>") 104 + if templ_7745c5c3_Err != nil { 105 + return templ_7745c5c3_Err 106 + } 107 + } 108 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"mt-6 grid md:grid-cols-[max-content_1fr] *:border-highlightMed\"><h3 class=\"px-6 pt-4 pb-2 md:py-4 border-t bg-overlay md:border-r text-subtle font-bold\">Client ID</h3><p class=\"px-6 pb-4 pt-1 md:py-4 md:border-t bg-overlay min-w-0 overflow-x-scroll\">") 109 + if templ_7745c5c3_Err != nil { 110 + return templ_7745c5c3_Err 111 + } 112 + var templ_7745c5c3_Var5 string 113 + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 114 + if templ_7745c5c3_Err != nil { 115 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 30, Col: 101} 116 + } 117 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 118 + if templ_7745c5c3_Err != nil { 119 + return templ_7745c5c3_Err 120 + } 121 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><h3 class=\"px-6 pt-4 pb-2 md:py-4 border-t md:border-b md:border-r text-subtle font-bold\">Redirect URL</h3><p class=\"px-6 pb-4 pt-1 md:py-4 md:border-y min-w-0 overflow-x-scroll\">") 122 + if templ_7745c5c3_Err != nil { 123 + return templ_7745c5c3_Err 124 + } 125 + var templ_7745c5c3_Var6 string 126 + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 127 + if templ_7745c5c3_Err != nil { 128 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 32, Col: 93} 129 + } 130 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 131 + if templ_7745c5c3_Err != nil { 132 + return templ_7745c5c3_Err 133 + } 134 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><h3 class=\"px-6 pt-4 pb-2 md:py-4 border-t bg-overlay md:border-r text-subtle font-bold\">Scopes</h3><ul class=\"px-6 pb-4 pt-1 md:py-4 bg-overlay flex flex-wrap gap-3\">") 135 + if templ_7745c5c3_Err != nil { 136 + return templ_7745c5c3_Err 137 + } 138 + for _, scope := range req.Scopes { 139 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"px-2 py-1 text-sm bg-pine rounded\">") 140 + if templ_7745c5c3_Err != nil { 141 + return templ_7745c5c3_Err 142 + } 143 + var templ_7745c5c3_Var7 string 144 + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(scope) 145 + if templ_7745c5c3_Err != nil { 146 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 36, Col: 59} 147 + } 148 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 149 + if templ_7745c5c3_Err != nil { 150 + return templ_7745c5c3_Err 151 + } 152 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>") 153 + if templ_7745c5c3_Err != nil { 154 + return templ_7745c5c3_Err 155 + } 156 + } 157 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></div></div><form method=\"post\" action=\"/authorization/accept\"><input type=\"hidden\" name=\"response_type\" value=\"code\"> <input type=\"hidden\" name=\"scope\" value=\"") 158 + if templ_7745c5c3_Err != nil { 159 + return templ_7745c5c3_Err 160 + } 161 + var templ_7745c5c3_Var8 string 162 + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(req.Scopes, " ")) 163 + if templ_7745c5c3_Err != nil { 164 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 43, Col: 74} 165 + } 166 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) 167 + if templ_7745c5c3_Err != nil { 168 + return templ_7745c5c3_Err 169 + } 170 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"redirect_uri\" value=\"") 171 + if templ_7745c5c3_Err != nil { 172 + return templ_7745c5c3_Err 173 + } 174 + var templ_7745c5c3_Var9 string 175 + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 176 + if templ_7745c5c3_Err != nil { 177 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 44, Col: 67} 178 + } 179 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 180 + if templ_7745c5c3_Err != nil { 181 + return templ_7745c5c3_Err 182 + } 183 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"client_id\" value=\"") 184 + if templ_7745c5c3_Err != nil { 185 + return templ_7745c5c3_Err 186 + } 187 + var templ_7745c5c3_Var10 string 188 + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 189 + if templ_7745c5c3_Err != nil { 190 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 45, Col: 61} 191 + } 192 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 193 + if templ_7745c5c3_Err != nil { 194 + return templ_7745c5c3_Err 195 + } 196 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"state\" value=\"") 197 + if templ_7745c5c3_Err != nil { 198 + return templ_7745c5c3_Err 199 + } 200 + var templ_7745c5c3_Var11 string 201 + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(req.State) 202 + if templ_7745c5c3_Err != nil { 203 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 46, Col: 54} 204 + } 205 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 206 + if templ_7745c5c3_Err != nil { 207 + return templ_7745c5c3_Err 208 + } 209 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"code_challenge\" value=\"") 210 + if templ_7745c5c3_Err != nil { 211 + return templ_7745c5c3_Err 212 + } 213 + var templ_7745c5c3_Var12 string 214 + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallenge) 215 + if templ_7745c5c3_Err != nil { 216 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 47, Col: 71} 217 + } 218 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) 219 + if templ_7745c5c3_Err != nil { 220 + return templ_7745c5c3_Err 221 + } 222 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"code_challenge_method\" value=\"") 223 + if templ_7745c5c3_Err != nil { 224 + return templ_7745c5c3_Err 225 + } 226 + var templ_7745c5c3_Var13 string 227 + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallengeMethod) 228 + if templ_7745c5c3_Err != nil { 229 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 48, Col: 84} 230 + } 231 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) 232 + if templ_7745c5c3_Err != nil { 233 + return templ_7745c5c3_Err 234 + } 235 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><input type=\"hidden\" name=\"nonce_id\" value=\"") 236 + if templ_7745c5c3_Err != nil { 237 + return templ_7745c5c3_Err 238 + } 239 + var templ_7745c5c3_Var14 string 240 + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(nonceId) 241 + if templ_7745c5c3_Err != nil { 242 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 50, Col: 55} 243 + } 244 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) 245 + if templ_7745c5c3_Err != nil { 246 + return templ_7745c5c3_Err 247 + } 248 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"nonce\" value=\"") 249 + if templ_7745c5c3_Err != nil { 250 + return templ_7745c5c3_Err 251 + } 252 + var templ_7745c5c3_Var15 string 253 + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(nonce) 254 + if templ_7745c5c3_Err != nil { 255 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 51, Col: 50} 256 + } 257 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) 258 + if templ_7745c5c3_Err != nil { 259 + return templ_7745c5c3_Err 260 + } 261 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <button class=\"mt-8 px-3 py-2 flex items-center justify-center gap-2 bg-surface border border-highlightMed text-sm font-bold transition rounded hover:bg-foam hover:text-surface\" id=\"submit\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"size-4\"><path fill-rule=\"evenodd\" d=\"M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z\" clip-rule=\"evenodd\"></path></svg> Authorize</button></form></main>") 262 + if templ_7745c5c3_Err != nil { 263 + return templ_7745c5c3_Err 264 + } 265 + return templ_7745c5c3_Err 266 + }) 267 + }
+23
html/pages/home.templ
··· 1 + package pages 2 + 3 + import ( 4 + p "github.com/puregarlic/space/html/components/posts" 5 + "github.com/puregarlic/space/models" 6 + ) 7 + 8 + templ Home(posts []*models.Post) { 9 + <ul class="flex flex-col gap-6"> 10 + if len(posts) > 0 { 11 + for _, post := range posts { 12 + <li class="flex flex-col"> 13 + @p.PostFeedHeader(post) 14 + @p.PostContent(post) 15 + </li> 16 + } 17 + } else { 18 + <li class="text-muted bg-surface px-4 py-8 text-center border border-overlay"> 19 + intention-rich, content-poor 20 + </li> 21 + } 22 + </ul> 23 + }
+69
html/pages/home_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package pages 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import ( 12 + p "github.com/puregarlic/space/html/components/posts" 13 + "github.com/puregarlic/space/models" 14 + ) 15 + 16 + func Home(posts []*models.Post) templ.Component { 17 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 18 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 19 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 20 + if !templ_7745c5c3_IsBuffer { 21 + defer func() { 22 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 23 + if templ_7745c5c3_Err == nil { 24 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 25 + } 26 + }() 27 + } 28 + ctx = templ.InitializeContext(ctx) 29 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 30 + if templ_7745c5c3_Var1 == nil { 31 + templ_7745c5c3_Var1 = templ.NopComponent 32 + } 33 + ctx = templ.ClearChildren(ctx) 34 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<ul class=\"flex flex-col gap-6\">") 35 + if templ_7745c5c3_Err != nil { 36 + return templ_7745c5c3_Err 37 + } 38 + if len(posts) > 0 { 39 + for _, post := range posts { 40 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"flex flex-col\">") 41 + if templ_7745c5c3_Err != nil { 42 + return templ_7745c5c3_Err 43 + } 44 + templ_7745c5c3_Err = p.PostFeedHeader(post).Render(ctx, templ_7745c5c3_Buffer) 45 + if templ_7745c5c3_Err != nil { 46 + return templ_7745c5c3_Err 47 + } 48 + templ_7745c5c3_Err = p.PostContent(post).Render(ctx, templ_7745c5c3_Buffer) 49 + if templ_7745c5c3_Err != nil { 50 + return templ_7745c5c3_Err 51 + } 52 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>") 53 + if templ_7745c5c3_Err != nil { 54 + return templ_7745c5c3_Err 55 + } 56 + } 57 + } else { 58 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"text-muted bg-surface px-4 py-8 text-center border border-overlay\">intention-rich, content-poor</li>") 59 + if templ_7745c5c3_Err != nil { 60 + return templ_7745c5c3_Err 61 + } 62 + } 63 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>") 64 + if templ_7745c5c3_Err != nil { 65 + return templ_7745c5c3_Err 66 + } 67 + return templ_7745c5c3_Err 68 + }) 69 + }
+27
html/pages/post.templ
··· 1 + package pages 2 + 3 + import ( 4 + "github.com/puregarlic/space/html/components/posts" 5 + "github.com/puregarlic/space/models" 6 + ) 7 + 8 + templ Post(post *models.Post) { 9 + <div class="flex flex-col gap-8"> 10 + <a href="/" class="text-sm text-muted flex items-center gap-1"> 11 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4"> 12 + <path fill-rule="evenodd" d="M14 8a.75.75 0 0 1-.75.75H4.56l1.22 1.22a.75.75 0 1 1-1.06 1.06l-2.5-2.5a.75.75 0 0 1 0-1.06l2.5-2.5a.75.75 0 0 1 1.06 1.06L4.56 7.25h8.69A.75.75 0 0 1 14 8Z" clip-rule="evenodd"></path> 13 + </svg> 14 + back to home 15 + </a> 16 + <div> 17 + @posts.PostContent(post) 18 + </div> 19 + @posts.PostDetails(post) 20 + <div class="py-12 flex flex-col gap-1 items-center text-muted text-xs font-light"> 21 + interactions not implemented yet 22 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4"> 23 + <path fill-rule="evenodd" d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0ZM6 8c.552 0 1-.672 1-1.5S6.552 5 6 5s-1 .672-1 1.5S5.448 8 6 8Zm5-1.5c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5Zm-6.005 5.805a.75.75 0 0 0 1.06 0 2.75 2.75 0 0 1 3.89 0 .75.75 0 0 0 1.06-1.06 4.25 4.25 0 0 0-6.01 0 .75.75 0 0 0 0 1.06Z" clip-rule="evenodd"></path> 24 + </svg> 25 + </div> 26 + </div> 27 + }
+56
html/pages/post_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package pages 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import ( 12 + "github.com/puregarlic/space/html/components/posts" 13 + "github.com/puregarlic/space/models" 14 + ) 15 + 16 + func Post(post *models.Post) templ.Component { 17 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 18 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 19 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 20 + if !templ_7745c5c3_IsBuffer { 21 + defer func() { 22 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 23 + if templ_7745c5c3_Err == nil { 24 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 25 + } 26 + }() 27 + } 28 + ctx = templ.InitializeContext(ctx) 29 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 30 + if templ_7745c5c3_Var1 == nil { 31 + templ_7745c5c3_Var1 = templ.NopComponent 32 + } 33 + ctx = templ.ClearChildren(ctx) 34 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex flex-col gap-8\"><a href=\"/\" class=\"text-sm text-muted flex items-center gap-1\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"size-4\"><path fill-rule=\"evenodd\" d=\"M14 8a.75.75 0 0 1-.75.75H4.56l1.22 1.22a.75.75 0 1 1-1.06 1.06l-2.5-2.5a.75.75 0 0 1 0-1.06l2.5-2.5a.75.75 0 0 1 1.06 1.06L4.56 7.25h8.69A.75.75 0 0 1 14 8Z\" clip-rule=\"evenodd\"></path></svg> back to home</a><div>") 35 + if templ_7745c5c3_Err != nil { 36 + return templ_7745c5c3_Err 37 + } 38 + templ_7745c5c3_Err = posts.PostContent(post).Render(ctx, templ_7745c5c3_Buffer) 39 + if templ_7745c5c3_Err != nil { 40 + return templ_7745c5c3_Err 41 + } 42 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") 43 + if templ_7745c5c3_Err != nil { 44 + return templ_7745c5c3_Err 45 + } 46 + templ_7745c5c3_Err = posts.PostDetails(post).Render(ctx, templ_7745c5c3_Buffer) 47 + if templ_7745c5c3_Err != nil { 48 + return templ_7745c5c3_Err 49 + } 50 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"py-12 flex flex-col gap-1 items-center text-muted text-xs font-light\">interactions not implemented yet <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"size-4\"><path fill-rule=\"evenodd\" d=\"M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0ZM6 8c.552 0 1-.672 1-1.5S6.552 5 6 5s-1 .672-1 1.5S5.448 8 6 8Zm5-1.5c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5Zm-6.005 5.805a.75.75 0 0 0 1.06 0 2.75 2.75 0 0 1 3.89 0 .75.75 0 0 0 1.06-1.06 4.25 4.25 0 0 0-6.01 0 .75.75 0 0 0 0 1.06Z\" clip-rule=\"evenodd\"></path></svg></div></div>") 51 + if templ_7745c5c3_Err != nil { 52 + return templ_7745c5c3_Err 53 + } 54 + return templ_7745c5c3_Err 55 + }) 56 + }
-241
indieauth.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "crypto/sha256" 6 - "crypto/subtle" 7 - "errors" 8 - "net/http" 9 - "net/url" 10 - "os" 11 - "strings" 12 - "time" 13 - 14 - "github.com/puregarlic/space/pages" 15 - 16 - "github.com/a-h/templ" 17 - "github.com/aidarkhanov/nanoid" 18 - "github.com/golang-jwt/jwt/v5" 19 - "go.hacdias.com/indielib/indieauth" 20 - ) 21 - 22 - // storeAuthorization stores the authorization request and returns a code for it. 23 - // Something such as JWT tokens could be used in a production environment. 24 - func (s *server) storeAuthorization(req *indieauth.AuthenticationRequest) string { 25 - code := nanoid.New() 26 - 27 - s.db.Authorization.Set(code, req, 0) 28 - 29 - return code 30 - } 31 - 32 - type CustomTokenClaims struct { 33 - Scopes []string `json:"scopes"` 34 - jwt.RegisteredClaims 35 - } 36 - 37 - type contextKey string 38 - 39 - const ( 40 - scopesContextKey contextKey = "scopes" 41 - ) 42 - 43 - // authorizationGetHandler handles the GET method for the authorization endpoint. 44 - func (s *server) authorizationGetHandler(w http.ResponseWriter, r *http.Request) { 45 - // In a production server, this page would usually be protected. In order for 46 - // the user to authorize this request, they must be authenticated. This could 47 - // be done in different ways: username/password, passkeys, etc. 48 - 49 - // Parse the authorization request. 50 - req, err := s.ias.ParseAuthorization(r) 51 - if err != nil { 52 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 53 - return 54 - } 55 - 56 - // Do a best effort attempt at fetching more information about the application 57 - // that we can show to the user. Not all applications provide this sort of 58 - // information. 59 - app, _ := s.ias.DiscoverApplicationMetadata(r.Context(), req.ClientID) 60 - 61 - // Here, we just display a small HTML document where the user has to press 62 - // to authorize this request. Please note that this template contains a form 63 - // where we dump all the request information. This makes it possible to reuse 64 - // [indieauth.Server.ParseAuthorization] when the user authorizes the request. 65 - templ.Handler(pages.Auth(req, app)).ServeHTTP(w, r) 66 - } 67 - 68 - // authorizationPostHandler handles the POST method for the authorization endpoint. 69 - func (s *server) authorizationPostHandler(w http.ResponseWriter, r *http.Request) { 70 - s.authorizationCodeExchange(w, r, false) 71 - } 72 - 73 - // tokenHandler handles the token endpoint. In our case, we only accept the default 74 - // type which is exchanging an authorization code for a token. 75 - func (s *server) tokenHandler(w http.ResponseWriter, r *http.Request) { 76 - if r.Method != http.MethodPost { 77 - httpError(w, http.StatusMethodNotAllowed) 78 - return 79 - } 80 - 81 - if r.Form.Get("grant_type") == "refresh_token" { 82 - // NOTE: this server does not implement refresh tokens. 83 - // https://indieauth.spec.indieweb.org/#refresh-tokens 84 - w.WriteHeader(http.StatusNotImplemented) 85 - return 86 - } 87 - 88 - s.authorizationCodeExchange(w, r, true) 89 - } 90 - 91 - type tokenResponse struct { 92 - Me string `json:"me"` 93 - AccessToken string `json:"access_token,omitempty"` 94 - TokenType string `json:"token_type,omitempty"` 95 - Scope string `json:"scope,omitempty"` 96 - ExpiresIn int64 `json:"expires_in,omitempty"` 97 - } 98 - 99 - // authorizationCodeExchange handles the authorization code exchange. It is used by 100 - // both the authorization handler to exchange the code for the user's profile URL, 101 - // and by the token endpoint, to exchange the code by a token. 102 - func (s *server) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) { 103 - if err := r.ParseForm(); err != nil { 104 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 105 - return 106 - } 107 - 108 - // t := s.getAuthorization(r.Form.Get("code")) 109 - req, present := s.db.Authorization.GetAndDelete(r.Form.Get("code")) 110 - if !present { 111 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization") 112 - return 113 - } 114 - authRequest := req.Value() 115 - 116 - err := s.ias.ValidateTokenExchange(authRequest, r) 117 - if err != nil { 118 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 119 - return 120 - } 121 - 122 - response := &tokenResponse{ 123 - Me: s.profileURL, 124 - } 125 - 126 - scopes := authRequest.Scopes 127 - 128 - if withToken { 129 - now := time.Now() 130 - expiresAt := now.Add(15 * time.Minute) 131 - claims := CustomTokenClaims{ 132 - scopes, 133 - jwt.RegisteredClaims{ 134 - ExpiresAt: jwt.NewNumericDate(expiresAt), 135 - IssuedAt: jwt.NewNumericDate(now), 136 - NotBefore: jwt.NewNumericDate(now), 137 - }, 138 - } 139 - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 140 - 141 - secret := os.Getenv("JWT_SECRET") 142 - jwt, err := token.SignedString([]byte(secret)) 143 - if err != nil { 144 - panic(err) 145 - } 146 - 147 - response.AccessToken = jwt 148 - response.TokenType = "Bearer" 149 - response.ExpiresIn = int64(time.Until(expiresAt).Seconds()) 150 - response.Scope = strings.Join(scopes, " ") 151 - } 152 - 153 - // An actual server may want to include the "profile" in the response if the 154 - // scope "profile" is included. 155 - serveJSON(w, http.StatusOK, response) 156 - } 157 - 158 - func (s *server) authorizationAcceptHandler(w http.ResponseWriter, r *http.Request) { 159 - // Parse authorization information. This only works because our authorization page 160 - // includes all the required information. This can be done in other ways: database, 161 - // whether temporary or not, cookies, etc. 162 - req, err := s.ias.ParseAuthorization(r) 163 - if err != nil { 164 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 165 - return 166 - } 167 - 168 - // Generate a random code and persist the information associated to that code. 169 - // You could do this in other ways: database, or JWT tokens, or both, for example. 170 - code := s.storeAuthorization(req) 171 - 172 - // Redirect to client callback. 173 - query := url.Values{} 174 - query.Set("code", code) 175 - query.Set("state", req.State) 176 - http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound) 177 - } 178 - 179 - // mustAuth is a middleware to ensure that the request is authorized. The way this 180 - // works depends on the implementation. It then stores the scopes in the context. 181 - func (s *server) mustAuth(next http.Handler) http.Handler { 182 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 183 - tokenStr := r.Header.Get("Authorization") 184 - tokenStr = strings.TrimPrefix(tokenStr, "Bearer") 185 - tokenStr = strings.TrimSpace(tokenStr) 186 - 187 - if len(tokenStr) <= 0 { 188 - serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials") 189 - return 190 - } 191 - 192 - token, err := jwt.ParseWithClaims(tokenStr, &CustomTokenClaims{}, func(t *jwt.Token) (interface{}, error) { 193 - return []byte(os.Getenv("JWT_SECRET")), nil 194 - }) 195 - 196 - if err != nil { 197 - serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token") 198 - return 199 - } else if claims, ok := token.Claims.(*CustomTokenClaims); ok { 200 - ctx := context.WithValue(r.Context(), scopesContextKey, claims.Scopes) 201 - next.ServeHTTP(w, r.WithContext(ctx)) 202 - return 203 - } else { 204 - serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims") 205 - return 206 - } 207 - }) 208 - } 209 - 210 - func (s *server) mustBasicAuth(next http.Handler) http.Handler { 211 - user, ok := os.LookupEnv("ADMIN_USERNAME") 212 - if !ok { 213 - panic(errors.New("ADMIN_USERNAME is not set, cannot start")) 214 - } 215 - 216 - pass, ok := os.LookupEnv("ADMIN_PASSWORD") 217 - if !ok { 218 - panic(errors.New("ADMIN_PASSWORD is not set, cannot start")) 219 - } 220 - 221 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 222 - username, password, ok := r.BasicAuth() 223 - if ok { 224 - usernameHash := sha256.Sum256([]byte(username)) 225 - passwordHash := sha256.Sum256([]byte(password)) 226 - expectedUsernameHash := sha256.Sum256([]byte(user)) 227 - expectedPasswordHash := sha256.Sum256([]byte(pass)) 228 - 229 - usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) 230 - passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) 231 - 232 - if usernameMatch && passwordMatch { 233 - next.ServeHTTP(w, r) 234 - return 235 - } 236 - } 237 - 238 - w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) 239 - http.Error(w, "Unauthorized", http.StatusUnauthorized) 240 - }) 241 - }
+44 -137
main.go
··· 1 1 package main 2 2 3 + //go:generate templ generate 4 + //go:generate deno run --allow-all npm:tailwindcss -i config/main.css -o static/styles.css -c config/tailwind.config.ts --minify 5 + 3 6 import ( 4 - "encoding/json" 5 - "flag" 7 + "os" 6 8 "time" 7 9 8 10 "log" 9 11 "net/http" 10 12 "strconv" 11 13 12 - "github.com/ostafen/clover/v2/query" 14 + "github.com/puregarlic/space/handlers" 15 + "github.com/puregarlic/space/models" 16 + "github.com/puregarlic/space/storage" 13 17 14 - "github.com/puregarlic/space/db" 15 - "github.com/puregarlic/space/pages" 16 - "github.com/puregarlic/space/types" 17 - 18 - "github.com/a-h/templ" 19 18 "github.com/go-chi/chi/v5" 20 19 "github.com/go-chi/chi/v5/middleware" 20 + "github.com/go-chi/cors" 21 21 "github.com/go-chi/httprate" 22 22 23 23 "go.hacdias.com/indielib/indieauth" 24 - "go.hacdias.com/indielib/microformats" 25 - "go.hacdias.com/indielib/micropub" 26 24 27 25 _ "github.com/joho/godotenv/autoload" 28 26 ) 29 27 30 28 func main() { 31 - // Setup flags. 32 - portPtr := flag.Int("port", 80, "port to listen on") 33 - addressPtr := flag.String("profile", "http://localhost/", "client URL and front facing address to listen on") 34 - flag.Parse() 35 - 36 - profileURL := *addressPtr 29 + port, profileURL := validateRuntimeConfiguration() 30 + defer storage.CleanupCaches() 37 31 38 - // Validate the given Client ID before starting the HTTP server. 39 - err := indieauth.IsValidProfileURL(profileURL) 40 - if err != nil { 41 - log.Fatal(err) 42 - } 43 - 44 - // Setup storage handlers 45 - store := db.NewStorage() 46 - defer store.Cleanup() 47 - 48 - // Create a new client. 49 - s := &server{ 50 - profileURL: profileURL, 51 - ias: indieauth.NewServer(true, nil), 52 - db: store, 53 - } 32 + storage.GORM().AutoMigrate(&models.Post{}) 54 33 55 34 r := chi.NewRouter() 56 35 ··· 58 37 r.Use(middleware.RealIP) 59 38 r.Use(middleware.Logger) 60 39 r.Use(middleware.Recoverer) 61 - 62 40 r.Use(httprate.LimitByIP(100, 1*time.Minute)) 63 41 64 - // Static resources 65 - r.Get("/static/*", http.StripPrefix("/static", http.FileServer(http.Dir("static"))).ServeHTTP) 42 + // CORS be enabled for browser-based agents to fetch `rel` elements. 43 + // We'll enable it just on the root route since it should be used as the profile URL 44 + r.With(cors.AllowAll().Handler).Get("/", handlers.ServeHomePage) 66 45 67 - // Pages 68 - r.Get("/", s.serveHomeTemplate) 69 - r.Get("/posts/{slug}", s.servePostTemplate) 46 + // Content pages 47 + r.Get("/posts/{slug}", handlers.ServePostPage) 70 48 71 - // IndieAuth handlers 72 - r.Group(func(r chi.Router) { 73 - r.Post("/token", s.tokenHandler) 74 - r.Post("/authorization", s.authorizationPostHandler) 75 - r.Post("/authorization/accept", s.authorizationAcceptHandler) 49 + // Static asset handlers 50 + r.Get("/media/*", handlers.ServeMedia) 51 + r.Get("/static/*", http.StripPrefix( 52 + "/static", 53 + http.FileServer(http.Dir("static")), 54 + ).ServeHTTP) 76 55 77 - // User authentication portal 78 - r.With(s.mustBasicAuth).Get("/authorization", s.authorizationGetHandler) 79 - }) 80 - 81 - // Micropub handler 82 - r.Route("/micropub", func(r chi.Router) { 83 - r.Use(s.mustAuth) 84 - r.Get("/", s.serveMicropub) 85 - r.Post("/", s.serveMicropub) 86 - }) 56 + // Service handlers 57 + handlers.AttachIndieAuth(r, "/authorization", profileURL) 58 + handlers.AttachMicropub(r, "/micropub", profileURL) 87 59 88 60 // Start it! 89 - log.Printf("Listening on http://localhost:%d", *portPtr) 61 + log.Printf("Listening on http://localhost:%d", port) 90 62 log.Printf("Listening on %s", profileURL) 91 - if err := http.ListenAndServe(":"+strconv.Itoa(*portPtr), r); err != nil { 63 + if err := http.ListenAndServe(":"+strconv.Itoa(port), r); err != nil { 92 64 log.Fatal(err) 93 65 } 94 66 } 95 67 96 - type server struct { 97 - profileURL string 98 - ias *indieauth.Server 99 - db *db.Storage 100 - } 68 + func validateRuntimeConfiguration() (portNumber int, profileURL string) { 69 + var port int 70 + if portStr, ok := os.LookupEnv("PORT"); !ok { 71 + port = 80 72 + } else { 73 + portInt, err := strconv.Atoi(portStr) 74 + if err != nil { 75 + log.Fatal(err) 76 + } 101 77 102 - func (s *server) serveHomeTemplate(w http.ResponseWriter, r *http.Request) { 103 - q := query.NewQuery( 104 - string(db.PostCollection), 105 - ).Sort(query.SortOption{ 106 - Field: "createdAt", 107 - Direction: -1, 108 - }).Limit(10) 109 - 110 - docs, err := s.db.Docs.FindAll(q) 111 - if err != nil { 112 - httpError(w, http.StatusInternalServerError) 113 - panic(err) 78 + port = portInt 114 79 } 115 80 116 - posts := make([]*types.Post, len(docs)) 117 - for i, doc := range docs { 118 - id := doc.ObjectId() 119 - post := &types.Post{ 120 - ID: id, 121 - } 122 - 123 - if err := doc.Unmarshal(post); err != nil { 124 - httpError(w, http.StatusInternalServerError) 125 - panic(err) 126 - } 127 - 128 - post.ID = id 129 - 130 - posts[i] = post 81 + profileURL, ok := os.LookupEnv("PROFILE_URL") 82 + if !ok { 83 + profileURL = "http://localhost/" 131 84 } 132 85 133 - templ.Handler(pages.Home(s.profileURL, posts)).ServeHTTP(w, r) 134 - } 135 - 136 - func (s *server) servePostTemplate(w http.ResponseWriter, r *http.Request) { 137 - id := chi.URLParam(r, "slug") 138 - post := &types.Post{} 139 - 140 - doc, err := s.db.Docs.FindById(string(db.PostCollection), id) 86 + // Validate the given Client ID before starting the HTTP server. 87 + err := indieauth.IsValidProfileURL(profileURL) 141 88 if err != nil { 142 - httpError(w, http.StatusInternalServerError) 143 - return 144 - } else if doc == nil { 145 - httpError(w, http.StatusNotFound) 146 - return 89 + log.Fatal(err) 147 90 } 148 91 149 - if err := doc.Unmarshal(post); err != nil { 150 - httpError(w, http.StatusInternalServerError) 151 - return 152 - } 153 - 154 - templ.Handler(pages.Post(post)).ServeHTTP(w, r) 155 - } 156 - 157 - func (s *server) serveMicropub(w http.ResponseWriter, r *http.Request) { 158 - micropub.NewHandler( 159 - &micropubImplementation{s}, 160 - micropub.WithGetPostTypes(func() []micropub.PostType { 161 - return []micropub.PostType{ 162 - { 163 - Name: "Post", 164 - Type: string(microformats.TypeNote), 165 - }, 166 - } 167 - }), 168 - ).ServeHTTP(w, r) 169 - } 170 - 171 - func httpError(w http.ResponseWriter, status int) { 172 - http.Error(w, http.StatusText(status), status) 173 - } 174 - 175 - func serveJSON(w http.ResponseWriter, code int, data interface{}) { 176 - w.Header().Set("Content-Type", "application/json; charset=utf-8") 177 - w.WriteHeader(code) 178 - _ = json.NewEncoder(w).Encode(data) 179 - } 180 - 181 - func serveErrorJSON(w http.ResponseWriter, code int, err, errDescription string) { 182 - serveJSON(w, code, map[string]string{ 183 - "error": err, 184 - "error_description": errDescription, 185 - }) 92 + return port, profileURL 186 93 }
-212
micropub.go
··· 1 - package main 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - "net/http" 7 - urlpkg "net/url" 8 - "reflect" 9 - "strings" 10 - "time" 11 - 12 - "github.com/ostafen/clover/v2/document" 13 - "github.com/puregarlic/space/db" 14 - "github.com/puregarlic/space/types" 15 - "github.com/samber/lo" 16 - 17 - "go.hacdias.com/indielib/micropub" 18 - ) 19 - 20 - type micropubImplementation struct { 21 - *server 22 - } 23 - 24 - func postIdFromUrlPath(path string) string { 25 - return strings.TrimPrefix(path, "/posts/") 26 - } 27 - 28 - func (s *micropubImplementation) HasScope(r *http.Request, scope string) bool { 29 - v := r.Context().Value(scopesContextKey) 30 - if scopes, ok := v.([]string); ok { 31 - for _, sc := range scopes { 32 - if sc == scope { 33 - return true 34 - } 35 - } 36 - } 37 - 38 - return false 39 - } 40 - 41 - func (s *micropubImplementation) Source(urlStr string) (map[string]any, error) { 42 - url, err := urlpkg.Parse(urlStr) 43 - if err != nil { 44 - return nil, fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 45 - } 46 - 47 - id := postIdFromUrlPath(url.Path) 48 - post := &types.Post{} 49 - doc, err := s.server.db.Docs.FindById(string(db.PostCollection), id) 50 - if err != nil { 51 - panic(err) 52 - } else if doc == nil { 53 - return nil, micropub.ErrNotFound 54 - } 55 - 56 - if err := doc.Unmarshal(post); err != nil { 57 - panic(err) 58 - } 59 - 60 - return map[string]any{ 61 - "type": []string{post.Type}, 62 - "properties": post.Properties, 63 - }, nil 64 - } 65 - 66 - func (s *micropubImplementation) SourceMany(limit, offset int) ([]map[string]any, error) { 67 - return nil, micropub.ErrNotImplemented 68 - } 69 - 70 - func (s *micropubImplementation) Create(req *micropub.Request) (string, error) { 71 - post := types.Post{ 72 - Type: req.Type, 73 - Properties: req.Properties, 74 - CreatedAt: time.Now().Unix(), 75 - } 76 - doc := document.NewDocumentOf(post) 77 - if doc == nil { 78 - return "", errors.New("Could not marshal post to Clover document") 79 - } 80 - 81 - id, err := s.server.db.Docs.InsertOne(string(db.PostCollection), doc) 82 - if err != nil { 83 - return "", err 84 - } 85 - 86 - return s.profileURL + "posts/" + id, nil 87 - } 88 - 89 - func (s *micropubImplementation) Update(req *micropub.Request) (string, error) { 90 - url, err := urlpkg.Parse(req.URL) 91 - if err != nil { 92 - return "", fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 93 - } 94 - 95 - id := postIdFromUrlPath(url.Path) 96 - 97 - if err := s.server.db.Docs.UpdateById( 98 - string(db.PostCollection), 99 - id, 100 - func(doc *document.Document) *document.Document { 101 - post := &types.Post{} 102 - if err := doc.Unmarshal(post); err != nil { 103 - panic(err) 104 - } 105 - 106 - props, err := updateProperties(post.Properties, req) 107 - if err != nil { 108 - panic(err) 109 - } 110 - 111 - doc.Set("properties", props) 112 - 113 - return doc 114 - }, 115 - ); err != nil { 116 - return "", fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 117 - } 118 - 119 - return s.profileURL + url.Path, nil 120 - } 121 - 122 - func (s *micropubImplementation) Delete(urlStr string) error { 123 - url, err := urlpkg.Parse(urlStr) 124 - if err != nil { 125 - return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 126 - } 127 - 128 - id := postIdFromUrlPath(url.Path) 129 - 130 - if err := s.server.db.Docs.DeleteById(string(db.PostCollection), id); err != nil { 131 - return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 132 - } 133 - 134 - return nil 135 - } 136 - 137 - func (s *micropubImplementation) Undelete(url string) error { 138 - return micropub.ErrNotImplemented 139 - } 140 - 141 - // updateProperties applies the updates (additions, deletions, replacements) 142 - // in the given [micropub.Request] to a set of existing microformats properties. 143 - func updateProperties(properties map[string][]any, req *micropub.Request) (map[string][]any, error) { 144 - if req.Updates.Replace != nil { 145 - for key, value := range req.Updates.Replace { 146 - properties[key] = value 147 - } 148 - } 149 - 150 - if req.Updates.Add != nil { 151 - for key, value := range req.Updates.Add { 152 - switch key { 153 - case "name": 154 - return nil, errors.New("cannot add a new name") 155 - case "content": 156 - return nil, errors.New("cannot add content") 157 - default: 158 - if key == "published" { 159 - if _, ok := properties["published"]; ok { 160 - return nil, errors.New("cannot replace published through add method") 161 - } 162 - } 163 - 164 - if _, ok := properties[key]; !ok { 165 - properties[key] = []any{} 166 - } 167 - 168 - properties[key] = append(properties[key], value...) 169 - } 170 - } 171 - } 172 - 173 - if req.Updates.Delete != nil { 174 - if reflect.TypeOf(req.Updates.Delete).Kind() == reflect.Slice { 175 - toDelete, ok := req.Updates.Delete.([]any) 176 - if !ok { 177 - return nil, errors.New("invalid delete array") 178 - } 179 - 180 - for _, key := range toDelete { 181 - delete(properties, fmt.Sprint(key)) 182 - } 183 - } else { 184 - toDelete, ok := req.Updates.Delete.(map[string]any) 185 - if !ok { 186 - return nil, fmt.Errorf("invalid delete object: expected map[string]any, got: %s", reflect.TypeOf(req.Updates.Delete)) 187 - } 188 - 189 - for key, v := range toDelete { 190 - value, ok := v.([]any) 191 - if !ok { 192 - return nil, fmt.Errorf("invalid value: expected []any, got: %s", reflect.TypeOf(value)) 193 - } 194 - 195 - if _, ok := properties[key]; !ok { 196 - properties[key] = []any{} 197 - } 198 - 199 - properties[key] = lo.Filter(properties[key], func(ss any, _ int) bool { 200 - for _, s := range value { 201 - if s == ss { 202 - return false 203 - } 204 - } 205 - return true 206 - }) 207 - } 208 - } 209 - } 210 - 211 - return properties, nil 212 - }
+25
models/post.go
··· 1 + package models 2 + 3 + import ( 4 + "time" 5 + 6 + "go.hacdias.com/indielib/microformats" 7 + "gorm.io/datatypes" 8 + "gorm.io/gorm" 9 + ) 10 + 11 + type Post struct { 12 + ID ULID `gorm:"primaryKey;unique"` 13 + 14 + Type string 15 + MicroformatType microformats.Type 16 + Properties datatypes.JSON 17 + 18 + CreatedAt time.Time 19 + UpdatedAt time.Time 20 + DeletedAt gorm.DeletedAt `gorm:"index"` 21 + } 22 + 23 + func (p *Post) Timestamp() string { 24 + return p.CreatedAt.Format("01/02/2006 at 3:04 PM") 25 + }
+80
models/ulid.go
··· 1 + package models 2 + 3 + import ( 4 + "database/sql/driver" 5 + 6 + "codeberg.org/gruf/go-ulid" 7 + "gorm.io/gorm" 8 + "gorm.io/gorm/schema" 9 + ) 10 + 11 + type ULID ulid.ULID 12 + 13 + func NewULID() ULID { 14 + return ULID(ulid.MustNew()) 15 + } 16 + 17 + func (ULID) GormDataType() string { 18 + return "string" 19 + } 20 + 21 + func (ULID) GormDBDataType(db *gorm.DB, field *schema.Field) string { 22 + switch db.Dialector.Name() { 23 + case "mysql": 24 + return "LONGTEXT" 25 + case "postgres": 26 + return "UUID" 27 + case "sqlserver": 28 + return "NVARCHAR" 29 + case "sqlite": 30 + return "TEXT" 31 + default: 32 + return "" 33 + } 34 + } 35 + 36 + func (u *ULID) Scan(value interface{}) error { 37 + var result ulid.ULID 38 + if err := result.Scan(value); err != nil { 39 + return err 40 + } 41 + *u = ULID(result) 42 + return nil 43 + } 44 + 45 + func (u ULID) Value() (driver.Value, error) { 46 + return ulid.ULID(u).String(), nil 47 + } 48 + 49 + func (u ULID) String() string { 50 + return ulid.ULID(u).String() 51 + } 52 + 53 + func (u ULID) Equals(other ULID) bool { 54 + return u.String() == other.String() 55 + } 56 + 57 + func (u ULID) Length() int { 58 + return len(u.String()) 59 + } 60 + 61 + func (u ULID) IsNil() bool { 62 + zero, err := ulid.ParseString("0000000000000000") 63 + if err != nil { 64 + panic(err) 65 + } 66 + 67 + return ulid.ULID(u) == zero 68 + } 69 + 70 + func (u ULID) IsEmpty() bool { 71 + return u.IsNil() || u.Length() == 0 72 + } 73 + 74 + func (u *ULID) IsNilPtr() bool { 75 + return u == nil 76 + } 77 + 78 + func (u *ULID) IsEmptyPtr() bool { 79 + return u.IsNilPtr() || u.IsEmpty() 80 + }
-15
package.json
··· 1 - { 2 - "name": "puregarlicspace", 3 - "version": "1.0.0", 4 - "description": "", 5 - "main": "index.js", 6 - "scripts": { 7 - "test": "echo \"Error: no test specified\" && exit 1" 8 - }, 9 - "keywords": [], 10 - "author": "", 11 - "license": "ISC", 12 - "devDependencies": { 13 - "tailwindcss": "^3.4.7" 14 - } 15 - }
-56
pages/auth.templ
··· 1 - package pages 2 - 3 - import "strings" 4 - 5 - import "go.hacdias.com/indielib/indieauth" 6 - 7 - templ Auth( req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata ) { 8 - <!DOCTYPE html> 9 - <html> 10 - <head> 11 - <title>Authorization | Micropub and IndieAuth Server Demo</title> 12 - </head> 13 - <body> 14 - <h1>IndieAuth Server Demo: Authorization</h1> 15 - 16 - <p> 17 - You received an authorization request from 18 - 19 - if app != nil { 20 - if len(app.Logo) > 0 { 21 - <img style="width: 1em; vertical-align: middle" src={ app.Logo } /> 22 - } 23 - 24 - <strong>{ app.Name }</strong> by { app.Author }: 25 - } else { 26 - the following client: 27 - } 28 - </p> 29 - 30 - <ul> 31 - <li><strong>Redirect:</strong> <code>{ req.ClientID }</code></li> 32 - <li><strong>Client:</strong> <code>{ req.RedirectURI }</code></li> 33 - </ul> 34 - 35 - <p>For the following scopes: 36 - for _, scope := range req.Scopes { 37 - <code>{ scope }</code> 38 - } 39 - .</p> 40 - 41 - <form method='post' action='/authorization/accept'> 42 - <input type="hidden" name="response_type" value="code"> 43 - <input type="hidden" name="scope" value={ strings.Join(req.Scopes, " ") }> 44 - <input type="hidden" name="redirect_uri" value={ req.RedirectURI }> 45 - <input type="hidden" name="client_id" value={ req.ClientID }> 46 - <input type="hidden" name="state" value={ req.State }> 47 - <input type="hidden" name="code_challenge" value={ req.CodeChallenge }> 48 - <input type="hidden" name="code_challenge_method" value={ req.CodeChallengeMethod }> 49 - 50 - <p>In a production server, this page could be behind some sort of authentication mechanism, such as username and password, PassKey, etc.</p> 51 - 52 - <button id="submit">Authorize</button> 53 - </form> 54 - </body> 55 - </html> 56 - }
-32
pages/home.templ
··· 1 - package pages 2 - 3 - import "github.com/puregarlic/space/types" 4 - import "fmt" 5 - 6 - templ Home(profileUrl string, posts []*types.Post) { 7 - <!DOCTYPE html> 8 - <html> 9 - <head> 10 - <title>Micropub and IndieAuth Server Demo</title> 11 - <link rel="authorization_endpoint" href="/authorization"> 12 - <link rel="token_endpoint" href="/token"> 13 - <link rel="micropub" href="/micropub"> 14 - 15 - <link rel="stylesheet" href="/static/styles.css" /> 16 - </head> 17 - <body> 18 - <h1>Micropub and IndieAuth Server Demo</h1> 19 - 20 - <p>Sign in on a website that supports IndieAuth. Use <code>{ profileUrl }</code> as your domain.</p> 21 - 22 - <h2>Posts</h2> 23 - 24 - <p>You can create posts using a Micropub client.</p> 25 - <ul> 26 - for _, post := range posts { 27 - <li><a href={ templ.URL("/posts/" + post.ID) }>{ post.ID }</a> - { fmt.Sprint(post.Properties["content"]) }</li> 28 - } 29 - </ul> 30 - </body> 31 - </html> 32 - }
-56
pages/post.templ
··· 1 - package pages 2 - 3 - import "github.com/puregarlic/space/types" 4 - import "fmt" 5 - import "encoding/json" 6 - import "reflect" 7 - 8 - func printProperty(post *types.Post, name string) string { 9 - if val, ok := post.Properties[name]; ok { 10 - tp := reflect.TypeOf(val) 11 - switch tp.Kind() { 12 - default: 13 - return fmt.Sprint(val) 14 - case reflect.Slice: 15 - str := "" 16 - for _, v := range val { 17 - str = str + fmt.Sprint(v) 18 - } 19 - 20 - return str 21 - } 22 - 23 - } 24 - 25 - return "<no name provided>" 26 - } 27 - 28 - func printPost(post *types.Post) string { 29 - out, err := json.Marshal(post) 30 - 31 - if (err != nil) { 32 - panic (err) 33 - } 34 - 35 - return fmt.Sprint(string(out)) 36 - } 37 - 38 - templ Post(post *types.Post) { 39 - <!DOCTYPE html> 40 - <html> 41 - <head> 42 - <title>Post | Micropub and IndieAuth Server Demo</title> 43 - </head> 44 - <body> 45 - <div class={ post.Type }> 46 - <h1 class="p-name">{ printProperty(post, "name") }</h1> 47 - <p class="p-content">{ printProperty(post, "content") }</p> 48 - 49 - <h3>Stored Microformats</h3> 50 - <code> 51 - <pre>{ printPost(post) }</pre> 52 - </code> 53 - </div> 54 - </body> 55 - </html> 56 - }
-842
pnpm-lock.yaml
··· 1 - lockfileVersion: '9.0' 2 - 3 - settings: 4 - autoInstallPeers: true 5 - excludeLinksFromLockfile: false 6 - 7 - importers: 8 - 9 - .: 10 - devDependencies: 11 - tailwindcss: 12 - specifier: ^3.4.7 13 - version: 3.4.7 14 - 15 - packages: 16 - 17 - '@alloc/quick-lru@5.2.0': 18 - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 19 - engines: {node: '>=10'} 20 - 21 - '@isaacs/cliui@8.0.2': 22 - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 23 - engines: {node: '>=12'} 24 - 25 - '@jridgewell/gen-mapping@0.3.5': 26 - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 27 - engines: {node: '>=6.0.0'} 28 - 29 - '@jridgewell/resolve-uri@3.1.2': 30 - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 31 - engines: {node: '>=6.0.0'} 32 - 33 - '@jridgewell/set-array@1.2.1': 34 - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 35 - engines: {node: '>=6.0.0'} 36 - 37 - '@jridgewell/sourcemap-codec@1.5.0': 38 - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 39 - 40 - '@jridgewell/trace-mapping@0.3.25': 41 - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 42 - 43 - '@nodelib/fs.scandir@2.1.5': 44 - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 45 - engines: {node: '>= 8'} 46 - 47 - '@nodelib/fs.stat@2.0.5': 48 - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 49 - engines: {node: '>= 8'} 50 - 51 - '@nodelib/fs.walk@1.2.8': 52 - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 53 - engines: {node: '>= 8'} 54 - 55 - '@pkgjs/parseargs@0.11.0': 56 - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 57 - engines: {node: '>=14'} 58 - 59 - ansi-regex@5.0.1: 60 - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 61 - engines: {node: '>=8'} 62 - 63 - ansi-regex@6.0.1: 64 - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} 65 - engines: {node: '>=12'} 66 - 67 - ansi-styles@4.3.0: 68 - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 69 - engines: {node: '>=8'} 70 - 71 - ansi-styles@6.2.1: 72 - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 73 - engines: {node: '>=12'} 74 - 75 - any-promise@1.3.0: 76 - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 77 - 78 - anymatch@3.1.3: 79 - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 80 - engines: {node: '>= 8'} 81 - 82 - arg@5.0.2: 83 - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 84 - 85 - balanced-match@1.0.2: 86 - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 87 - 88 - binary-extensions@2.3.0: 89 - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 90 - engines: {node: '>=8'} 91 - 92 - brace-expansion@2.0.1: 93 - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 94 - 95 - braces@3.0.3: 96 - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 97 - engines: {node: '>=8'} 98 - 99 - camelcase-css@2.0.1: 100 - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 101 - engines: {node: '>= 6'} 102 - 103 - chokidar@3.6.0: 104 - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 105 - engines: {node: '>= 8.10.0'} 106 - 107 - color-convert@2.0.1: 108 - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 109 - engines: {node: '>=7.0.0'} 110 - 111 - color-name@1.1.4: 112 - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 113 - 114 - commander@4.1.1: 115 - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 116 - engines: {node: '>= 6'} 117 - 118 - cross-spawn@7.0.3: 119 - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 120 - engines: {node: '>= 8'} 121 - 122 - cssesc@3.0.0: 123 - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 124 - engines: {node: '>=4'} 125 - hasBin: true 126 - 127 - didyoumean@1.2.2: 128 - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 129 - 130 - dlv@1.1.3: 131 - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 132 - 133 - eastasianwidth@0.2.0: 134 - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 135 - 136 - emoji-regex@8.0.0: 137 - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 138 - 139 - emoji-regex@9.2.2: 140 - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 141 - 142 - fast-glob@3.3.2: 143 - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 144 - engines: {node: '>=8.6.0'} 145 - 146 - fastq@1.17.1: 147 - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 148 - 149 - fill-range@7.1.1: 150 - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 151 - engines: {node: '>=8'} 152 - 153 - foreground-child@3.2.1: 154 - resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} 155 - engines: {node: '>=14'} 156 - 157 - fsevents@2.3.3: 158 - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 159 - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 160 - os: [darwin] 161 - 162 - function-bind@1.1.2: 163 - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 164 - 165 - glob-parent@5.1.2: 166 - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 167 - engines: {node: '>= 6'} 168 - 169 - glob-parent@6.0.2: 170 - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 171 - engines: {node: '>=10.13.0'} 172 - 173 - glob@10.4.5: 174 - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 175 - hasBin: true 176 - 177 - hasown@2.0.2: 178 - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 179 - engines: {node: '>= 0.4'} 180 - 181 - is-binary-path@2.1.0: 182 - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 183 - engines: {node: '>=8'} 184 - 185 - is-core-module@2.15.0: 186 - resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} 187 - engines: {node: '>= 0.4'} 188 - 189 - is-extglob@2.1.1: 190 - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 191 - engines: {node: '>=0.10.0'} 192 - 193 - is-fullwidth-code-point@3.0.0: 194 - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 195 - engines: {node: '>=8'} 196 - 197 - is-glob@4.0.3: 198 - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 199 - engines: {node: '>=0.10.0'} 200 - 201 - is-number@7.0.0: 202 - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 203 - engines: {node: '>=0.12.0'} 204 - 205 - isexe@2.0.0: 206 - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 207 - 208 - jackspeak@3.4.3: 209 - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 210 - 211 - jiti@1.21.6: 212 - resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} 213 - hasBin: true 214 - 215 - lilconfig@2.1.0: 216 - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 217 - engines: {node: '>=10'} 218 - 219 - lilconfig@3.1.2: 220 - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} 221 - engines: {node: '>=14'} 222 - 223 - lines-and-columns@1.2.4: 224 - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 225 - 226 - lru-cache@10.4.3: 227 - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 228 - 229 - merge2@1.4.1: 230 - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 231 - engines: {node: '>= 8'} 232 - 233 - micromatch@4.0.7: 234 - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} 235 - engines: {node: '>=8.6'} 236 - 237 - minimatch@9.0.5: 238 - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 239 - engines: {node: '>=16 || 14 >=14.17'} 240 - 241 - minipass@7.1.2: 242 - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 243 - engines: {node: '>=16 || 14 >=14.17'} 244 - 245 - mz@2.7.0: 246 - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 247 - 248 - nanoid@3.3.7: 249 - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 250 - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 251 - hasBin: true 252 - 253 - normalize-path@3.0.0: 254 - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 255 - engines: {node: '>=0.10.0'} 256 - 257 - object-assign@4.1.1: 258 - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 259 - engines: {node: '>=0.10.0'} 260 - 261 - object-hash@3.0.0: 262 - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 263 - engines: {node: '>= 6'} 264 - 265 - package-json-from-dist@1.0.0: 266 - resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} 267 - 268 - path-key@3.1.1: 269 - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 270 - engines: {node: '>=8'} 271 - 272 - path-parse@1.0.7: 273 - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 274 - 275 - path-scurry@1.11.1: 276 - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 277 - engines: {node: '>=16 || 14 >=14.18'} 278 - 279 - picocolors@1.0.1: 280 - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} 281 - 282 - picomatch@2.3.1: 283 - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 284 - engines: {node: '>=8.6'} 285 - 286 - pify@2.3.0: 287 - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 288 - engines: {node: '>=0.10.0'} 289 - 290 - pirates@4.0.6: 291 - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 292 - engines: {node: '>= 6'} 293 - 294 - postcss-import@15.1.0: 295 - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 296 - engines: {node: '>=14.0.0'} 297 - peerDependencies: 298 - postcss: ^8.0.0 299 - 300 - postcss-js@4.0.1: 301 - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 302 - engines: {node: ^12 || ^14 || >= 16} 303 - peerDependencies: 304 - postcss: ^8.4.21 305 - 306 - postcss-load-config@4.0.2: 307 - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 308 - engines: {node: '>= 14'} 309 - peerDependencies: 310 - postcss: '>=8.0.9' 311 - ts-node: '>=9.0.0' 312 - peerDependenciesMeta: 313 - postcss: 314 - optional: true 315 - ts-node: 316 - optional: true 317 - 318 - postcss-nested@6.2.0: 319 - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} 320 - engines: {node: '>=12.0'} 321 - peerDependencies: 322 - postcss: ^8.2.14 323 - 324 - postcss-selector-parser@6.1.1: 325 - resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==} 326 - engines: {node: '>=4'} 327 - 328 - postcss-value-parser@4.2.0: 329 - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 330 - 331 - postcss@8.4.40: 332 - resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} 333 - engines: {node: ^10 || ^12 || >=14} 334 - 335 - queue-microtask@1.2.3: 336 - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 337 - 338 - read-cache@1.0.0: 339 - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 340 - 341 - readdirp@3.6.0: 342 - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 343 - engines: {node: '>=8.10.0'} 344 - 345 - resolve@1.22.8: 346 - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 347 - hasBin: true 348 - 349 - reusify@1.0.4: 350 - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 351 - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 352 - 353 - run-parallel@1.2.0: 354 - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 355 - 356 - shebang-command@2.0.0: 357 - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 358 - engines: {node: '>=8'} 359 - 360 - shebang-regex@3.0.0: 361 - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 362 - engines: {node: '>=8'} 363 - 364 - signal-exit@4.1.0: 365 - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 366 - engines: {node: '>=14'} 367 - 368 - source-map-js@1.2.0: 369 - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 370 - engines: {node: '>=0.10.0'} 371 - 372 - string-width@4.2.3: 373 - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 374 - engines: {node: '>=8'} 375 - 376 - string-width@5.1.2: 377 - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 378 - engines: {node: '>=12'} 379 - 380 - strip-ansi@6.0.1: 381 - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 382 - engines: {node: '>=8'} 383 - 384 - strip-ansi@7.1.0: 385 - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 386 - engines: {node: '>=12'} 387 - 388 - sucrase@3.35.0: 389 - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 390 - engines: {node: '>=16 || 14 >=14.17'} 391 - hasBin: true 392 - 393 - supports-preserve-symlinks-flag@1.0.0: 394 - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 395 - engines: {node: '>= 0.4'} 396 - 397 - tailwindcss@3.4.7: 398 - resolution: {integrity: sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==} 399 - engines: {node: '>=14.0.0'} 400 - hasBin: true 401 - 402 - thenify-all@1.6.0: 403 - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 404 - engines: {node: '>=0.8'} 405 - 406 - thenify@3.3.1: 407 - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 408 - 409 - to-regex-range@5.0.1: 410 - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 411 - engines: {node: '>=8.0'} 412 - 413 - ts-interface-checker@0.1.13: 414 - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 415 - 416 - util-deprecate@1.0.2: 417 - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 418 - 419 - which@2.0.2: 420 - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 421 - engines: {node: '>= 8'} 422 - hasBin: true 423 - 424 - wrap-ansi@7.0.0: 425 - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 426 - engines: {node: '>=10'} 427 - 428 - wrap-ansi@8.1.0: 429 - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 430 - engines: {node: '>=12'} 431 - 432 - yaml@2.5.0: 433 - resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} 434 - engines: {node: '>= 14'} 435 - hasBin: true 436 - 437 - snapshots: 438 - 439 - '@alloc/quick-lru@5.2.0': {} 440 - 441 - '@isaacs/cliui@8.0.2': 442 - dependencies: 443 - string-width: 5.1.2 444 - string-width-cjs: string-width@4.2.3 445 - strip-ansi: 7.1.0 446 - strip-ansi-cjs: strip-ansi@6.0.1 447 - wrap-ansi: 8.1.0 448 - wrap-ansi-cjs: wrap-ansi@7.0.0 449 - 450 - '@jridgewell/gen-mapping@0.3.5': 451 - dependencies: 452 - '@jridgewell/set-array': 1.2.1 453 - '@jridgewell/sourcemap-codec': 1.5.0 454 - '@jridgewell/trace-mapping': 0.3.25 455 - 456 - '@jridgewell/resolve-uri@3.1.2': {} 457 - 458 - '@jridgewell/set-array@1.2.1': {} 459 - 460 - '@jridgewell/sourcemap-codec@1.5.0': {} 461 - 462 - '@jridgewell/trace-mapping@0.3.25': 463 - dependencies: 464 - '@jridgewell/resolve-uri': 3.1.2 465 - '@jridgewell/sourcemap-codec': 1.5.0 466 - 467 - '@nodelib/fs.scandir@2.1.5': 468 - dependencies: 469 - '@nodelib/fs.stat': 2.0.5 470 - run-parallel: 1.2.0 471 - 472 - '@nodelib/fs.stat@2.0.5': {} 473 - 474 - '@nodelib/fs.walk@1.2.8': 475 - dependencies: 476 - '@nodelib/fs.scandir': 2.1.5 477 - fastq: 1.17.1 478 - 479 - '@pkgjs/parseargs@0.11.0': 480 - optional: true 481 - 482 - ansi-regex@5.0.1: {} 483 - 484 - ansi-regex@6.0.1: {} 485 - 486 - ansi-styles@4.3.0: 487 - dependencies: 488 - color-convert: 2.0.1 489 - 490 - ansi-styles@6.2.1: {} 491 - 492 - any-promise@1.3.0: {} 493 - 494 - anymatch@3.1.3: 495 - dependencies: 496 - normalize-path: 3.0.0 497 - picomatch: 2.3.1 498 - 499 - arg@5.0.2: {} 500 - 501 - balanced-match@1.0.2: {} 502 - 503 - binary-extensions@2.3.0: {} 504 - 505 - brace-expansion@2.0.1: 506 - dependencies: 507 - balanced-match: 1.0.2 508 - 509 - braces@3.0.3: 510 - dependencies: 511 - fill-range: 7.1.1 512 - 513 - camelcase-css@2.0.1: {} 514 - 515 - chokidar@3.6.0: 516 - dependencies: 517 - anymatch: 3.1.3 518 - braces: 3.0.3 519 - glob-parent: 5.1.2 520 - is-binary-path: 2.1.0 521 - is-glob: 4.0.3 522 - normalize-path: 3.0.0 523 - readdirp: 3.6.0 524 - optionalDependencies: 525 - fsevents: 2.3.3 526 - 527 - color-convert@2.0.1: 528 - dependencies: 529 - color-name: 1.1.4 530 - 531 - color-name@1.1.4: {} 532 - 533 - commander@4.1.1: {} 534 - 535 - cross-spawn@7.0.3: 536 - dependencies: 537 - path-key: 3.1.1 538 - shebang-command: 2.0.0 539 - which: 2.0.2 540 - 541 - cssesc@3.0.0: {} 542 - 543 - didyoumean@1.2.2: {} 544 - 545 - dlv@1.1.3: {} 546 - 547 - eastasianwidth@0.2.0: {} 548 - 549 - emoji-regex@8.0.0: {} 550 - 551 - emoji-regex@9.2.2: {} 552 - 553 - fast-glob@3.3.2: 554 - dependencies: 555 - '@nodelib/fs.stat': 2.0.5 556 - '@nodelib/fs.walk': 1.2.8 557 - glob-parent: 5.1.2 558 - merge2: 1.4.1 559 - micromatch: 4.0.7 560 - 561 - fastq@1.17.1: 562 - dependencies: 563 - reusify: 1.0.4 564 - 565 - fill-range@7.1.1: 566 - dependencies: 567 - to-regex-range: 5.0.1 568 - 569 - foreground-child@3.2.1: 570 - dependencies: 571 - cross-spawn: 7.0.3 572 - signal-exit: 4.1.0 573 - 574 - fsevents@2.3.3: 575 - optional: true 576 - 577 - function-bind@1.1.2: {} 578 - 579 - glob-parent@5.1.2: 580 - dependencies: 581 - is-glob: 4.0.3 582 - 583 - glob-parent@6.0.2: 584 - dependencies: 585 - is-glob: 4.0.3 586 - 587 - glob@10.4.5: 588 - dependencies: 589 - foreground-child: 3.2.1 590 - jackspeak: 3.4.3 591 - minimatch: 9.0.5 592 - minipass: 7.1.2 593 - package-json-from-dist: 1.0.0 594 - path-scurry: 1.11.1 595 - 596 - hasown@2.0.2: 597 - dependencies: 598 - function-bind: 1.1.2 599 - 600 - is-binary-path@2.1.0: 601 - dependencies: 602 - binary-extensions: 2.3.0 603 - 604 - is-core-module@2.15.0: 605 - dependencies: 606 - hasown: 2.0.2 607 - 608 - is-extglob@2.1.1: {} 609 - 610 - is-fullwidth-code-point@3.0.0: {} 611 - 612 - is-glob@4.0.3: 613 - dependencies: 614 - is-extglob: 2.1.1 615 - 616 - is-number@7.0.0: {} 617 - 618 - isexe@2.0.0: {} 619 - 620 - jackspeak@3.4.3: 621 - dependencies: 622 - '@isaacs/cliui': 8.0.2 623 - optionalDependencies: 624 - '@pkgjs/parseargs': 0.11.0 625 - 626 - jiti@1.21.6: {} 627 - 628 - lilconfig@2.1.0: {} 629 - 630 - lilconfig@3.1.2: {} 631 - 632 - lines-and-columns@1.2.4: {} 633 - 634 - lru-cache@10.4.3: {} 635 - 636 - merge2@1.4.1: {} 637 - 638 - micromatch@4.0.7: 639 - dependencies: 640 - braces: 3.0.3 641 - picomatch: 2.3.1 642 - 643 - minimatch@9.0.5: 644 - dependencies: 645 - brace-expansion: 2.0.1 646 - 647 - minipass@7.1.2: {} 648 - 649 - mz@2.7.0: 650 - dependencies: 651 - any-promise: 1.3.0 652 - object-assign: 4.1.1 653 - thenify-all: 1.6.0 654 - 655 - nanoid@3.3.7: {} 656 - 657 - normalize-path@3.0.0: {} 658 - 659 - object-assign@4.1.1: {} 660 - 661 - object-hash@3.0.0: {} 662 - 663 - package-json-from-dist@1.0.0: {} 664 - 665 - path-key@3.1.1: {} 666 - 667 - path-parse@1.0.7: {} 668 - 669 - path-scurry@1.11.1: 670 - dependencies: 671 - lru-cache: 10.4.3 672 - minipass: 7.1.2 673 - 674 - picocolors@1.0.1: {} 675 - 676 - picomatch@2.3.1: {} 677 - 678 - pify@2.3.0: {} 679 - 680 - pirates@4.0.6: {} 681 - 682 - postcss-import@15.1.0(postcss@8.4.40): 683 - dependencies: 684 - postcss: 8.4.40 685 - postcss-value-parser: 4.2.0 686 - read-cache: 1.0.0 687 - resolve: 1.22.8 688 - 689 - postcss-js@4.0.1(postcss@8.4.40): 690 - dependencies: 691 - camelcase-css: 2.0.1 692 - postcss: 8.4.40 693 - 694 - postcss-load-config@4.0.2(postcss@8.4.40): 695 - dependencies: 696 - lilconfig: 3.1.2 697 - yaml: 2.5.0 698 - optionalDependencies: 699 - postcss: 8.4.40 700 - 701 - postcss-nested@6.2.0(postcss@8.4.40): 702 - dependencies: 703 - postcss: 8.4.40 704 - postcss-selector-parser: 6.1.1 705 - 706 - postcss-selector-parser@6.1.1: 707 - dependencies: 708 - cssesc: 3.0.0 709 - util-deprecate: 1.0.2 710 - 711 - postcss-value-parser@4.2.0: {} 712 - 713 - postcss@8.4.40: 714 - dependencies: 715 - nanoid: 3.3.7 716 - picocolors: 1.0.1 717 - source-map-js: 1.2.0 718 - 719 - queue-microtask@1.2.3: {} 720 - 721 - read-cache@1.0.0: 722 - dependencies: 723 - pify: 2.3.0 724 - 725 - readdirp@3.6.0: 726 - dependencies: 727 - picomatch: 2.3.1 728 - 729 - resolve@1.22.8: 730 - dependencies: 731 - is-core-module: 2.15.0 732 - path-parse: 1.0.7 733 - supports-preserve-symlinks-flag: 1.0.0 734 - 735 - reusify@1.0.4: {} 736 - 737 - run-parallel@1.2.0: 738 - dependencies: 739 - queue-microtask: 1.2.3 740 - 741 - shebang-command@2.0.0: 742 - dependencies: 743 - shebang-regex: 3.0.0 744 - 745 - shebang-regex@3.0.0: {} 746 - 747 - signal-exit@4.1.0: {} 748 - 749 - source-map-js@1.2.0: {} 750 - 751 - string-width@4.2.3: 752 - dependencies: 753 - emoji-regex: 8.0.0 754 - is-fullwidth-code-point: 3.0.0 755 - strip-ansi: 6.0.1 756 - 757 - string-width@5.1.2: 758 - dependencies: 759 - eastasianwidth: 0.2.0 760 - emoji-regex: 9.2.2 761 - strip-ansi: 7.1.0 762 - 763 - strip-ansi@6.0.1: 764 - dependencies: 765 - ansi-regex: 5.0.1 766 - 767 - strip-ansi@7.1.0: 768 - dependencies: 769 - ansi-regex: 6.0.1 770 - 771 - sucrase@3.35.0: 772 - dependencies: 773 - '@jridgewell/gen-mapping': 0.3.5 774 - commander: 4.1.1 775 - glob: 10.4.5 776 - lines-and-columns: 1.2.4 777 - mz: 2.7.0 778 - pirates: 4.0.6 779 - ts-interface-checker: 0.1.13 780 - 781 - supports-preserve-symlinks-flag@1.0.0: {} 782 - 783 - tailwindcss@3.4.7: 784 - dependencies: 785 - '@alloc/quick-lru': 5.2.0 786 - arg: 5.0.2 787 - chokidar: 3.6.0 788 - didyoumean: 1.2.2 789 - dlv: 1.1.3 790 - fast-glob: 3.3.2 791 - glob-parent: 6.0.2 792 - is-glob: 4.0.3 793 - jiti: 1.21.6 794 - lilconfig: 2.1.0 795 - micromatch: 4.0.7 796 - normalize-path: 3.0.0 797 - object-hash: 3.0.0 798 - picocolors: 1.0.1 799 - postcss: 8.4.40 800 - postcss-import: 15.1.0(postcss@8.4.40) 801 - postcss-js: 4.0.1(postcss@8.4.40) 802 - postcss-load-config: 4.0.2(postcss@8.4.40) 803 - postcss-nested: 6.2.0(postcss@8.4.40) 804 - postcss-selector-parser: 6.1.1 805 - resolve: 1.22.8 806 - sucrase: 3.35.0 807 - transitivePeerDependencies: 808 - - ts-node 809 - 810 - thenify-all@1.6.0: 811 - dependencies: 812 - thenify: 3.3.1 813 - 814 - thenify@3.3.1: 815 - dependencies: 816 - any-promise: 1.3.0 817 - 818 - to-regex-range@5.0.1: 819 - dependencies: 820 - is-number: 7.0.0 821 - 822 - ts-interface-checker@0.1.13: {} 823 - 824 - util-deprecate@1.0.2: {} 825 - 826 - which@2.0.2: 827 - dependencies: 828 - isexe: 2.0.0 829 - 830 - wrap-ansi@7.0.0: 831 - dependencies: 832 - ansi-styles: 4.3.0 833 - string-width: 4.2.3 834 - strip-ansi: 6.0.1 835 - 836 - wrap-ansi@8.1.0: 837 - dependencies: 838 - ansi-styles: 6.2.1 839 - string-width: 5.1.2 840 - strip-ansi: 7.1.0 841 - 842 - yaml@2.5.0: {}
+15
scripts/run.sh
··· 1 + #!/bin/bash 2 + set -e 3 + 4 + echo "Starting script" 5 + 6 + # Restore the database if it does not already exist. 7 + if [ -f /data/data.db ]; then 8 + echo "Database already exists, skipping restore" 9 + else 10 + echo "No database found, restoring from replica if exists" 11 + litestream restore -if-replica-exists /data/data.db 12 + fi 13 + 14 + # Run litestream with your app as the subprocess. 15 + exec litestream replicate -exec "/space"
+246
services/indieauth.go
··· 1 + package services 2 + 3 + import ( 4 + "context" 5 + "crypto/sha256" 6 + "crypto/subtle" 7 + "encoding/json" 8 + "errors" 9 + "net/http" 10 + "net/url" 11 + "os" 12 + "strings" 13 + "time" 14 + 15 + "github.com/puregarlic/space/html/layouts" 16 + "github.com/puregarlic/space/html/pages" 17 + "github.com/puregarlic/space/storage" 18 + 19 + "github.com/aidarkhanov/nanoid" 20 + "github.com/golang-jwt/jwt/v5" 21 + "go.hacdias.com/indielib/indieauth" 22 + ) 23 + 24 + type IndieAuth struct { 25 + ProfileURL string 26 + Server *indieauth.Server 27 + } 28 + 29 + func (i *IndieAuth) storeAuthorization(req *indieauth.AuthenticationRequest) string { 30 + code := nanoid.New() 31 + 32 + storage.AuthCache().Set(code, req, 0) 33 + 34 + return code 35 + } 36 + 37 + type CustomTokenClaims struct { 38 + Scopes []string `json:"scopes"` 39 + jwt.RegisteredClaims 40 + } 41 + 42 + type contextKey string 43 + 44 + const ( 45 + scopesContextKey contextKey = "scopes" 46 + ) 47 + 48 + func (i *IndieAuth) HandleAuthGET(w http.ResponseWriter, r *http.Request) { 49 + req, err := i.Server.ParseAuthorization(r) 50 + if err != nil { 51 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 52 + return 53 + } 54 + 55 + app, _ := i.Server.DiscoverApplicationMetadata(r.Context(), req.ClientID) 56 + 57 + nonceId, nonce := nanoid.New(), nanoid.New() 58 + storage.NonceCache().Set(nonceId, nonce, 0) 59 + 60 + layouts.RenderDefault("authorize", pages.Auth(req, app, nonceId, nonce)).ServeHTTP(w, r) 61 + } 62 + 63 + func (i *IndieAuth) HandleAuthPOST(w http.ResponseWriter, r *http.Request) { 64 + i.authorizationCodeExchange(w, r, false) 65 + } 66 + 67 + func (i *IndieAuth) HandleToken(w http.ResponseWriter, r *http.Request) { 68 + if r.Method != http.MethodPost { 69 + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 70 + return 71 + } 72 + 73 + if r.Form.Get("grant_type") == "refresh_token" { 74 + // NOTE: this server does not implement refresh tokens. 75 + // https://indieauth.spec.indieweb.org/#refresh-tokens 76 + w.WriteHeader(http.StatusNotImplemented) 77 + return 78 + } 79 + 80 + i.authorizationCodeExchange(w, r, true) 81 + } 82 + 83 + type tokenResponse struct { 84 + Me string `json:"me"` 85 + AccessToken string `json:"access_token,omitempty"` 86 + TokenType string `json:"token_type,omitempty"` 87 + Scope string `json:"scope,omitempty"` 88 + ExpiresIn int64 `json:"expires_in,omitempty"` 89 + } 90 + 91 + func (i *IndieAuth) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) { 92 + if err := r.ParseForm(); err != nil { 93 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 94 + return 95 + } 96 + 97 + // t := s.getAuthorization(r.Form.Get("code")) 98 + req, present := storage.AuthCache().GetAndDelete(r.Form.Get("code")) 99 + if !present { 100 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization") 101 + return 102 + } 103 + authRequest := req.Value() 104 + 105 + err := i.Server.ValidateTokenExchange(authRequest, r) 106 + if err != nil { 107 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 108 + return 109 + } 110 + 111 + response := &tokenResponse{ 112 + Me: i.ProfileURL, 113 + } 114 + 115 + scopes := authRequest.Scopes 116 + 117 + if withToken { 118 + now := time.Now() 119 + expiresAt := now.Add(15 * time.Minute) 120 + claims := CustomTokenClaims{ 121 + scopes, 122 + jwt.RegisteredClaims{ 123 + ExpiresAt: jwt.NewNumericDate(expiresAt), 124 + IssuedAt: jwt.NewNumericDate(now), 125 + NotBefore: jwt.NewNumericDate(now), 126 + }, 127 + } 128 + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 129 + 130 + secret := os.Getenv("JWT_SECRET") 131 + jwt, err := token.SignedString([]byte(secret)) 132 + if err != nil { 133 + panic(err) 134 + } 135 + 136 + response.AccessToken = jwt 137 + response.TokenType = "Bearer" 138 + response.ExpiresIn = int64(time.Until(expiresAt).Seconds()) 139 + response.Scope = strings.Join(scopes, " ") 140 + } 141 + 142 + // An actual server may want to include the "profile" in the response if the 143 + // scope "profile" is included. 144 + SendJSON(w, http.StatusOK, response) 145 + } 146 + 147 + func (i *IndieAuth) HandleAuthApproval(w http.ResponseWriter, r *http.Request) { 148 + id := r.FormValue("nonce_id") 149 + nonce := r.FormValue("nonce") 150 + 151 + stored, ok := storage.NonceCache().GetAndDelete(id) 152 + if !ok { 153 + SendErrorJSON(w, http.StatusBadRequest, "bad_request", "nonce does not match") 154 + } else if stored.Value() != nonce { 155 + SendErrorJSON(w, http.StatusBadRequest, "bad_request", "nonce does not match") 156 + } 157 + 158 + req, err := i.Server.ParseAuthorization(r) 159 + if err != nil { 160 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 161 + return 162 + } 163 + 164 + code := i.storeAuthorization(req) 165 + 166 + // Redirect to client callback. 167 + query := url.Values{} 168 + query.Set("code", code) 169 + query.Set("state", req.State) 170 + http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound) 171 + } 172 + 173 + func MustAuth(next http.Handler) http.Handler { 174 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 175 + tokenStr := r.Header.Get("Authorization") 176 + tokenStr = strings.TrimPrefix(tokenStr, "Bearer") 177 + tokenStr = strings.TrimSpace(tokenStr) 178 + 179 + if len(tokenStr) <= 0 { 180 + SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials") 181 + return 182 + } 183 + 184 + token, err := jwt.ParseWithClaims(tokenStr, &CustomTokenClaims{}, func(t *jwt.Token) (interface{}, error) { 185 + return []byte(os.Getenv("JWT_SECRET")), nil 186 + }) 187 + 188 + if err != nil { 189 + SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token") 190 + return 191 + } else if claims, ok := token.Claims.(*CustomTokenClaims); ok { 192 + ctx := context.WithValue(r.Context(), scopesContextKey, claims.Scopes) 193 + next.ServeHTTP(w, r.WithContext(ctx)) 194 + return 195 + } else { 196 + SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims") 197 + return 198 + } 199 + }) 200 + } 201 + 202 + func MustBasicAuth(next http.Handler) http.Handler { 203 + user, ok := os.LookupEnv("ADMIN_USERNAME") 204 + if !ok { 205 + panic(errors.New("ADMIN_USERNAME is not set, cannot start")) 206 + } 207 + 208 + pass, ok := os.LookupEnv("ADMIN_PASSWORD") 209 + if !ok { 210 + panic(errors.New("ADMIN_PASSWORD is not set, cannot start")) 211 + } 212 + 213 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 214 + username, password, ok := r.BasicAuth() 215 + if ok { 216 + usernameHash := sha256.Sum256([]byte(username)) 217 + passwordHash := sha256.Sum256([]byte(password)) 218 + expectedUsernameHash := sha256.Sum256([]byte(user)) 219 + expectedPasswordHash := sha256.Sum256([]byte(pass)) 220 + 221 + usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) 222 + passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) 223 + 224 + if usernameMatch && passwordMatch { 225 + next.ServeHTTP(w, r) 226 + return 227 + } 228 + } 229 + 230 + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) 231 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 232 + }) 233 + } 234 + 235 + func SendJSON(w http.ResponseWriter, code int, data interface{}) { 236 + w.Header().Set("Content-Type", "application/json; charset=utf-8") 237 + w.WriteHeader(code) 238 + _ = json.NewEncoder(w).Encode(data) 239 + } 240 + 241 + func SendErrorJSON(w http.ResponseWriter, code int, err, errDescription string) { 242 + SendJSON(w, code, map[string]string{ 243 + "error": err, 244 + "error_description": errDescription, 245 + }) 246 + }
+270
services/micropub.go
··· 1 + package services 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "mime/multipart" 9 + "net/http" 10 + urlpkg "net/url" 11 + "os" 12 + "reflect" 13 + "strings" 14 + 15 + "github.com/puregarlic/space/models" 16 + "github.com/puregarlic/space/storage" 17 + 18 + "github.com/aidarkhanov/nanoid" 19 + "github.com/aws/aws-sdk-go-v2/aws" 20 + "github.com/aws/aws-sdk-go-v2/service/s3" 21 + "github.com/h2non/filetype" 22 + "github.com/samber/lo" 23 + 24 + "go.hacdias.com/indielib/microformats" 25 + "go.hacdias.com/indielib/micropub" 26 + ) 27 + 28 + type Micropub struct { 29 + ProfileURL string 30 + } 31 + 32 + func postIdFromUrlPath(path string) string { 33 + return strings.TrimPrefix(path, "/posts/") 34 + } 35 + 36 + func (m *Micropub) HasScope(r *http.Request, scope string) bool { 37 + v := r.Context().Value(scopesContextKey) 38 + if scopes, ok := v.([]string); ok { 39 + for _, sc := range scopes { 40 + if sc == scope { 41 + return true 42 + } 43 + } 44 + } 45 + 46 + return false 47 + } 48 + 49 + func (m *Micropub) Source(urlStr string) (map[string]any, error) { 50 + url, err := urlpkg.Parse(urlStr) 51 + if err != nil { 52 + return nil, fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 53 + } 54 + 55 + id := postIdFromUrlPath(url.Path) 56 + post := &models.Post{} 57 + 58 + res := storage.GORM().Find(post, "id = ?", id) 59 + if res.Error != nil { 60 + panic(res.Error) 61 + } else if res.RowsAffected == 0 { 62 + return nil, micropub.ErrNotFound 63 + } 64 + 65 + return map[string]any{ 66 + "type": []string{post.Type}, 67 + "properties": post.Properties, 68 + }, nil 69 + } 70 + 71 + func (m *Micropub) SourceMany(limit, offset int) ([]map[string]any, error) { 72 + return nil, micropub.ErrNotImplemented 73 + } 74 + 75 + func (m *Micropub) HandleMediaUpload(file multipart.File, header *multipart.FileHeader) (string, error) { 76 + defer file.Close() 77 + 78 + kind, err := filetype.MatchReader(file) 79 + if _, err := file.Seek(0, 0); err != nil { 80 + return "", fmt.Errorf("%w: %w", errors.New("failed to reset cursor"), err) 81 + } 82 + 83 + if err != nil { 84 + return "", fmt.Errorf("%w: %w", errors.New("failed to upload"), err) 85 + } 86 + 87 + key := fmt.Sprintf("media/%s.%s", nanoid.New(), kind.Extension) 88 + _, err = storage.S3().PutObject(context.TODO(), &s3.PutObjectInput{ 89 + Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), 90 + Key: &key, 91 + Body: file, 92 + }) 93 + if err != nil { 94 + return "", fmt.Errorf("%w: %w", errors.New("failed to upload"), err) 95 + } 96 + 97 + return m.ProfileURL + key, nil 98 + } 99 + 100 + func (m *Micropub) Create(req *micropub.Request) (string, error) { 101 + props, err := json.Marshal(req.Properties) 102 + if err != nil { 103 + return "", err 104 + } 105 + 106 + mfType, _ := microformats.DiscoverType(map[string]any{ 107 + "type": req.Type, 108 + "properties": req.Properties, 109 + }) 110 + 111 + post := &models.Post{ 112 + ID: models.NewULID(), 113 + Type: req.Type, 114 + MicroformatType: mfType, 115 + Properties: props, 116 + } 117 + 118 + res := storage.GORM().Create(post) 119 + if res.Error != nil { 120 + return "", res.Error 121 + } 122 + 123 + return m.ProfileURL + "posts/" + post.ID.String(), nil 124 + } 125 + 126 + func (m *Micropub) Update(req *micropub.Request) (string, error) { 127 + url, err := urlpkg.Parse(req.URL) 128 + if err != nil { 129 + return "", fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 130 + } 131 + 132 + id := postIdFromUrlPath(url.Path) 133 + post := &models.Post{} 134 + 135 + res := storage.GORM().Find(post, "id = ?", id) 136 + if res.Error != nil { 137 + panic(res.Error) 138 + } else if res.RowsAffected != 1 { 139 + return "", micropub.ErrNotFound 140 + } 141 + 142 + newProps, err := updateProperties(json.RawMessage(post.Properties), req) 143 + if err != nil { 144 + panic(err) 145 + } 146 + 147 + post.Properties = newProps 148 + 149 + storage.GORM().Save(post) 150 + 151 + return url.String(), nil 152 + } 153 + 154 + func (m *Micropub) Delete(urlStr string) error { 155 + url, err := urlpkg.Parse(urlStr) 156 + if err != nil { 157 + return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 158 + } 159 + 160 + id := postIdFromUrlPath(url.Path) 161 + 162 + res := storage.GORM().Delete(&models.Post{}, "id = ?", id) 163 + if res.Error != nil { 164 + panic(res.Error) 165 + } else if res.RowsAffected == 0 { 166 + return fmt.Errorf("%w: %w", micropub.ErrNotFound, err) 167 + } 168 + 169 + return nil 170 + } 171 + 172 + func (m *Micropub) Undelete(urlStr string) error { 173 + url, err := urlpkg.Parse(urlStr) 174 + if err != nil { 175 + return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 176 + } 177 + 178 + id := postIdFromUrlPath(url.Path) 179 + res := storage.GORM().Unscoped().Model(&models.Post{}).Where("id = ?", id).Update("deleted_at", nil) 180 + if res.Error != nil { 181 + return res.Error 182 + } else if res.RowsAffected != 1 { 183 + return micropub.ErrNotFound 184 + } 185 + 186 + return nil 187 + } 188 + 189 + // updateProperties applies the updates (additions, deletions, replacements) 190 + // in the given [micropub.Request] to a set of existing microformats properties. 191 + func updateProperties(props json.RawMessage, req *micropub.Request) ([]byte, error) { 192 + properties := make(map[string][]any) 193 + if err := json.Unmarshal(props, &properties); err != nil { 194 + panic(err) 195 + } 196 + 197 + if req.Updates.Replace != nil { 198 + for key, value := range req.Updates.Replace { 199 + properties[key] = value 200 + } 201 + } 202 + 203 + if req.Updates.Add != nil { 204 + for key, value := range req.Updates.Add { 205 + switch key { 206 + case "name": 207 + return nil, errors.New("cannot add a new name") 208 + case "content": 209 + return nil, errors.New("cannot add content") 210 + default: 211 + if key == "published" { 212 + if _, ok := properties["published"]; ok { 213 + return nil, errors.New("cannot replace published through add method") 214 + } 215 + } 216 + 217 + if _, ok := properties[key]; !ok { 218 + properties[key] = []any{} 219 + } 220 + 221 + properties[key] = append(properties[key], value...) 222 + } 223 + } 224 + } 225 + 226 + if req.Updates.Delete != nil { 227 + if reflect.TypeOf(req.Updates.Delete).Kind() == reflect.Slice { 228 + toDelete, ok := req.Updates.Delete.([]any) 229 + if !ok { 230 + return nil, errors.New("invalid delete array") 231 + } 232 + 233 + for _, key := range toDelete { 234 + delete(properties, fmt.Sprint(key)) 235 + } 236 + } else { 237 + toDelete, ok := req.Updates.Delete.(map[string]any) 238 + if !ok { 239 + return nil, fmt.Errorf("invalid delete object: expected map[string]any, got: %s", reflect.TypeOf(req.Updates.Delete)) 240 + } 241 + 242 + for key, v := range toDelete { 243 + value, ok := v.([]any) 244 + if !ok { 245 + return nil, fmt.Errorf("invalid value: expected []any, got: %s", reflect.TypeOf(value)) 246 + } 247 + 248 + if _, ok := properties[key]; !ok { 249 + properties[key] = []any{} 250 + } 251 + 252 + properties[key] = lo.Filter(properties[key], func(ss any, _ int) bool { 253 + for _, s := range value { 254 + if s == ss { 255 + return false 256 + } 257 + } 258 + return true 259 + }) 260 + } 261 + } 262 + } 263 + 264 + propJson, err := json.Marshal(&properties) 265 + if err != nil { 266 + panic(err) 267 + } 268 + 269 + return propJson, nil 270 + }
+1
static/.gitkeep
··· 1 +
+47
storage/cache.go
··· 1 + package storage 2 + 3 + import ( 4 + "time" 5 + 6 + "github.com/jellydator/ttlcache/v3" 7 + "go.hacdias.com/indielib/indieauth" 8 + ) 9 + 10 + var authCache *ttlcache.Cache[string, *indieauth.AuthenticationRequest] 11 + var nonceCache *ttlcache.Cache[string, string] 12 + 13 + func CleanupCaches() { 14 + AuthCache().Stop() 15 + } 16 + 17 + func AuthCache() *ttlcache.Cache[string, *indieauth.AuthenticationRequest] { 18 + if authCache != nil { 19 + return authCache 20 + } 21 + 22 + cache := ttlcache.New( 23 + ttlcache.WithTTL[string, *indieauth.AuthenticationRequest](10 * time.Minute), 24 + ) 25 + 26 + go cache.Start() 27 + 28 + authCache = cache 29 + 30 + return cache 31 + } 32 + 33 + func NonceCache() *ttlcache.Cache[string, string] { 34 + if nonceCache != nil { 35 + return nonceCache 36 + } 37 + 38 + cache := ttlcache.New( 39 + ttlcache.WithTTL[string, string](5 * time.Minute), 40 + ) 41 + 42 + go cache.Start() 43 + 44 + nonceCache = cache 45 + 46 + return cache 47 + }
+37
storage/gorm.go
··· 1 + package storage 2 + 3 + import ( 4 + "log" 5 + "os" 6 + "path/filepath" 7 + 8 + "github.com/glebarez/sqlite" 9 + "gorm.io/gorm" 10 + ) 11 + 12 + var orm *gorm.DB 13 + 14 + func GORM() *gorm.DB { 15 + if orm != nil { 16 + return orm 17 + } 18 + 19 + dataDir := filepath.Join(".", "data") 20 + if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { 21 + log.Fatal(err) 22 + } 23 + 24 + db, err := gorm.Open(sqlite.Open(filepath.Join(dataDir, "data.db")), &gorm.Config{}) 25 + if err != nil { 26 + log.Fatal(err) 27 + } 28 + 29 + orm = db 30 + 31 + // TODO: Move migration to `models` package 32 + // if err := db.AutoMigrate(&models.Post{}); err != nil { 33 + // log.Fatal(err) 34 + // } 35 + 36 + return db 37 + }
+19
storage/rel.go
··· 1 + package storage 2 + 3 + type RelEntry struct { 4 + Name string 5 + HREF string 6 + } 7 + 8 + var registry = make([]*RelEntry, 0) 9 + 10 + func AddRel(name string, href string) { 11 + registry = append(registry, &RelEntry{ 12 + Name: name, 13 + HREF: href, 14 + }) 15 + } 16 + 17 + func GetRels() []*RelEntry { 18 + return registry 19 + }
+34
storage/s3.go
··· 1 + package storage 2 + 3 + import ( 4 + "context" 5 + "log" 6 + "os" 7 + 8 + "github.com/aws/aws-sdk-go-v2/aws" 9 + "github.com/aws/aws-sdk-go-v2/config" 10 + "github.com/aws/aws-sdk-go-v2/service/s3" 11 + ) 12 + 13 + var s3Client *s3.Client 14 + 15 + func S3() *s3.Client { 16 + if s3Client != nil { 17 + return s3Client 18 + } 19 + 20 + sdkConfig, err := config.LoadDefaultConfig(context.Background()) 21 + if err != nil { 22 + log.Printf("Couldn't load default configuration. Here's why: %v\n", err) 23 + panic(err) 24 + } 25 + 26 + svc := s3.NewFromConfig(sdkConfig, func(o *s3.Options) { 27 + o.BaseEndpoint = aws.String("https://" + os.Getenv("AWS_S3_ENDPOINT")) 28 + o.Region = os.Getenv("AWS_REGION") 29 + }) 30 + 31 + s3Client = svc 32 + 33 + return svc 34 + }
-11
tailwind.config.js
··· 1 - /** @type {import('tailwindcss').Config} */ 2 - module.exports = { 3 - content: [ 4 - "./**/*.{html,templ}" 5 - ], 6 - theme: { 7 - extend: {}, 8 - }, 9 - plugins: [], 10 - } 11 -
-8
types/post.go
··· 1 - package types 2 - 3 - type Post struct { 4 - ID string `clover:""` 5 - CreatedAt int64 `clover:"createdAt"` 6 - Type string `clover:"type"` 7 - Properties map[string][]any `clover:"properties"` 8 - }