Yōten: A social tracker for your language learning journey built on the atproto.

refactor: move computed data code to function to simplify visuals #21

merged opened by brookjeynes.dev targeting master from push-lokxzmtxxkxo
Labels

None yet.

Participants 1
AT URI
at://did:plc:4mj54vc4ha3lh32ksxwunnbh/sh.tangled.repo.pull/3m3c7vpu5pg22
+523 -290
Diff #0
+14
go.mod
··· 9 github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e 10 github.com/bluesky-social/jetstream v0.0.0-20250414024304-d17bd81a945e 11 github.com/carlmjohnson/versioninfo v0.22.5 12 github.com/go-chi/chi/v5 v5.2.1 13 github.com/gorilla/sessions v1.4.0 14 github.com/ipfs/go-cid v0.4.1 ··· 28 require ( 29 github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect 30 github.com/andybalholm/brotli v1.1.0 // indirect 31 github.com/beorn7/perks v1.0.1 // indirect 32 github.com/cenkalti/backoff/v4 v4.3.0 // indirect 33 github.com/cespare/xxhash/v2 v2.3.0 // indirect 34 github.com/cli/browser v1.3.0 // indirect 35 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 36 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 37 github.com/fatih/color v1.16.0 // indirect 38 github.com/felixge/httpsnoop v1.0.4 // indirect 39 github.com/fsnotify/fsnotify v1.7.0 // indirect 40 github.com/go-logr/logr v1.4.2 // indirect 41 github.com/go-logr/stdr v1.2.2 // indirect 42 github.com/goccy/go-json v0.10.2 // indirect ··· 69 github.com/lestrrat-go/httprc v1.0.4 // indirect 70 github.com/lestrrat-go/iter v1.0.2 // indirect 71 github.com/lestrrat-go/option v1.0.1 // indirect 72 github.com/mattn/go-colorable v0.1.13 // indirect 73 github.com/mattn/go-isatty v0.0.20 // indirect 74 github.com/minio/sha256-simd v1.0.1 // indirect 75 github.com/mr-tron/base58 v1.2.0 // indirect 76 github.com/multiformats/go-base32 v0.1.0 // indirect 77 github.com/multiformats/go-base36 v0.2.0 // indirect 78 github.com/multiformats/go-multibase v0.2.0 // indirect ··· 85 github.com/prometheus/client_model v0.6.1 // indirect 86 github.com/prometheus/common v0.54.0 // indirect 87 github.com/prometheus/procfs v0.15.1 // indirect 88 github.com/segmentio/asm v1.2.0 // indirect 89 github.com/spaolacci/murmur3 v1.1.0 // indirect 90 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 91 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 92 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect ··· 97 go.uber.org/multierr v1.11.0 // indirect 98 go.uber.org/zap v1.26.0 // indirect 99 golang.org/x/crypto v0.40.0 // indirect 100 golang.org/x/mod v0.26.0 // indirect 101 golang.org/x/sys v0.34.0 // indirect 102 golang.org/x/time v0.8.0 // indirect
··· 9 github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e 10 github.com/bluesky-social/jetstream v0.0.0-20250414024304-d17bd81a945e 11 github.com/carlmjohnson/versioninfo v0.22.5 12 + github.com/charmbracelet/log v0.4.2 13 github.com/go-chi/chi/v5 v5.2.1 14 github.com/gorilla/sessions v1.4.0 15 github.com/ipfs/go-cid v0.4.1 ··· 29 require ( 30 github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect 31 github.com/andybalholm/brotli v1.1.0 // indirect 32 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 33 github.com/beorn7/perks v1.0.1 // indirect 34 github.com/cenkalti/backoff/v4 v4.3.0 // indirect 35 github.com/cespare/xxhash/v2 v2.3.0 // indirect 36 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 37 + github.com/charmbracelet/lipgloss v1.1.0 // indirect 38 + github.com/charmbracelet/x/ansi v0.8.0 // indirect 39 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 40 + github.com/charmbracelet/x/term v0.2.1 // indirect 41 github.com/cli/browser v1.3.0 // indirect 42 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 43 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 44 github.com/fatih/color v1.16.0 // indirect 45 github.com/felixge/httpsnoop v1.0.4 // indirect 46 github.com/fsnotify/fsnotify v1.7.0 // indirect 47 + github.com/go-logfmt/logfmt v0.6.0 // indirect 48 github.com/go-logr/logr v1.4.2 // indirect 49 github.com/go-logr/stdr v1.2.2 // indirect 50 github.com/goccy/go-json v0.10.2 // indirect ··· 77 github.com/lestrrat-go/httprc v1.0.4 // indirect 78 github.com/lestrrat-go/iter v1.0.2 // indirect 79 github.com/lestrrat-go/option v1.0.1 // indirect 80 + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 81 github.com/mattn/go-colorable v0.1.13 // indirect 82 github.com/mattn/go-isatty v0.0.20 // indirect 83 + github.com/mattn/go-runewidth v0.0.16 // indirect 84 github.com/minio/sha256-simd v1.0.1 // indirect 85 github.com/mr-tron/base58 v1.2.0 // indirect 86 + github.com/muesli/termenv v0.16.0 // indirect 87 github.com/multiformats/go-base32 v0.1.0 // indirect 88 github.com/multiformats/go-base36 v0.2.0 // indirect 89 github.com/multiformats/go-multibase v0.2.0 // indirect ··· 96 github.com/prometheus/client_model v0.6.1 // indirect 97 github.com/prometheus/common v0.54.0 // indirect 98 github.com/prometheus/procfs v0.15.1 // indirect 99 + github.com/rivo/uniseg v0.4.7 // indirect 100 github.com/segmentio/asm v1.2.0 // indirect 101 github.com/spaolacci/murmur3 v1.1.0 // indirect 102 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 103 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 104 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 105 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect ··· 110 go.uber.org/multierr v1.11.0 // indirect 111 go.uber.org/zap v1.26.0 // indirect 112 golang.org/x/crypto v0.40.0 // indirect 113 + golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect 114 golang.org/x/mod v0.26.0 // indirect 115 golang.org/x/sys v0.34.0 // indirect 116 golang.org/x/time v0.8.0 // indirect
+27
go.sum
··· 5 github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= 6 github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 7 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 8 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 9 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= ··· 22 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 23 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 24 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 25 github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= 26 github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= 27 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= ··· 42 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 43 github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 44 github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 45 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 46 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 47 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= ··· 148 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 149 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 150 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 151 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 152 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 153 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 154 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 155 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 156 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 157 github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 158 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 159 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 160 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 161 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 162 github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 163 github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 164 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 165 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= ··· 196 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 197 github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= 198 github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 199 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 200 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 201 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= ··· 231 github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= 232 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 233 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 234 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 235 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 236 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
··· 5 github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= 6 github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 7 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 8 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 9 + github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 10 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 11 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 12 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= ··· 24 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 25 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 26 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 27 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 28 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 29 + github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 30 + github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 31 + github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= 32 + github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= 33 + github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 34 + github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 35 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 36 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 37 + github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 38 + github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 39 github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= 40 github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= 41 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= ··· 56 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 57 github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 58 github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 59 + github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 60 + github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 61 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 62 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 63 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= ··· 164 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 165 github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 166 github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 167 + github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 168 + github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 169 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 170 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 171 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 172 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 173 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 174 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 175 + github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 176 + github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 177 github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 178 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 179 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 180 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 181 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 182 github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 183 + github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 184 + github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 185 github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 186 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 187 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= ··· 218 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 219 github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= 220 github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 221 + github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 222 + github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 223 + github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 224 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 225 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 226 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= ··· 256 github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= 257 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 258 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 259 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 260 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 261 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 262 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 263 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+25 -7
internal/server/log/log.go
··· 4 "context" 5 "log/slog" 6 "os" 7 ) 8 9 - // NewHandler sets up a new slog.Handler with the service name as an attribute 10 func NewHandler(name string) slog.Handler { 11 - handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}) 12 - 13 - var attrs []slog.Attr 14 - attrs = append(attrs, slog.Attr{Key: "service", Value: slog.StringValue(name)}) 15 - handler.WithAttrs(attrs) 16 - return handler 17 } 18 19 func New(name string) *slog.Logger { ··· 45 46 return slog.Default() 47 }
··· 4 "context" 5 "log/slog" 6 "os" 7 + 8 + "github.com/charmbracelet/log" 9 ) 10 11 func NewHandler(name string) slog.Handler { 12 + return log.NewWithOptions(os.Stderr, log.Options{ 13 + ReportTimestamp: true, 14 + Prefix: name, 15 + Level: log.DebugLevel, 16 + }) 17 } 18 19 func New(name string) *slog.Logger { ··· 45 46 return slog.Default() 47 } 48 + 49 + // Sublogger derives a new logger from an existing one by appending a suffix to 50 + // its prefix. 51 + func SubLogger(base *slog.Logger, suffix string) *slog.Logger { 52 + // Try to get the underlying charmbracelet logger. 53 + if cl, ok := base.Handler().(*log.Logger); ok { 54 + prefix := cl.GetPrefix() 55 + if prefix != "" { 56 + prefix = prefix + "/" + suffix 57 + } else { 58 + prefix = suffix 59 + } 60 + return slog.New(NewHandler(prefix)) 61 + } 62 + 63 + // Fallback to no known handler type. 64 + return slog.New(NewHandler(suffix)) 65 + }
+96 -13
internal/server/oauth/handler.go
··· 1 2 - 3 import ( 4 "encoding/json" 5 - "log" 6 "net/http" 7 8 "github.com/go-chi/chi/v5" 9 10 11 12 13 ··· 41 42 43 44 - 45 - 46 - 47 - 48 - 49 jwks := o.Config.OAuth.Jwks 50 pubKey, err := pubKeyFromJwk(jwks) 51 if err != nil { 52 - log.Printf("failed to parse public key: %v", err) 53 http.Error(w, err.Error(), http.StatusInternalServerError) 54 return 55 } ··· 61 62 63 64 65 66 - 67 - 68 - 69 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) 70 if err != nil { 71 http.Error(w, err.Error(), http.StatusInternalServerError) 72 return 73 } 74 75 if err := o.SaveSession(w, r, sessData); err != nil { 76 http.Error(w, err.Error(), http.StatusInternalServerError) 77 return 78 } 79 80 81 82 83 - Event: ph.UserSignInSuccessEvent, 84 }) 85 if err != nil { 86 - log.Println("failed to enqueue posthog event:", err) 87 } 88 } 89
··· 1 + package oauth 2 3 import ( 4 + "context" 5 "encoding/json" 6 + "fmt" 7 "net/http" 8 + "time" 9 10 + comatproto "github.com/bluesky-social/indigo/api/atproto" 11 + lexutil "github.com/bluesky-social/indigo/lex/util" 12 "github.com/go-chi/chi/v5" 13 + "github.com/lestrrat-go/jwx/v2/jwk" 14 + "github.com/posthog/posthog-go" 15 16 + "yoten.app/api/yoten" 17 + ph "yoten.app/internal/clients/posthog" 18 + "yoten.app/internal/db" 19 + "yoten.app/internal/server/htmx" 20 + ) 21 22 + func (o *OAuth) Router() http.Handler { 23 24 25 ··· 53 54 55 56 jwks := o.Config.OAuth.Jwks 57 pubKey, err := pubKeyFromJwk(jwks) 58 if err != nil { 59 + o.Logger.Error("failed to parse public key", "err", err) 60 http.Error(w, err.Error(), http.StatusInternalServerError) 61 return 62 } ··· 68 69 70 71 + } 72 73 + func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { 74 + l := o.Logger.With("handler", "callback") 75 + ctx := r.Context() 76 77 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) 78 if err != nil { 79 + o.Logger.Error("failed to process callback", "err", err) 80 http.Error(w, err.Error(), http.StatusInternalServerError) 81 return 82 } 83 84 if err := o.SaveSession(w, r, sessData); err != nil { 85 + l.Error("failed to save session", "err", err) 86 http.Error(w, err.Error(), http.StatusInternalServerError) 87 return 88 } 89 90 + did := sessData.AccountDID.String() 91 + resolved, err := o.IdResolver.ResolveIdent(context.Background(), did) 92 + if err != nil { 93 + l.Error("failed to resolve handle", "handle", resolved.Handle.String(), "err", err) 94 + htmx.HxError(w, http.StatusBadRequest, fmt.Sprintf("'%s' is an invalid handle", resolved.Handle.String())) 95 + return 96 + } 97 98 + client, err := o.AuthorizedClient(r) 99 + if err != nil { 100 + l.Error("failed to get authorized client", "err", err) 101 + http.Error(w, err.Error(), http.StatusInternalServerError) 102 + return 103 + } 104 105 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", yoten.ActorProfileNSID, did, "self") 106 + var cid *string 107 + if ex != nil { 108 + cid = ex.Cid 109 + } 110 111 + // This should only occur once per account 112 + if ex == nil { 113 + createdAt := time.Now().Format(time.RFC3339) 114 + atresp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 115 + Collection: yoten.ActorProfileNSID, 116 + Repo: did, 117 + Rkey: "self", 118 + Record: &lexutil.LexiconTypeDecoder{ 119 + Val: &yoten.ActorProfile{ 120 + DisplayName: resolved.Handle.String(), 121 + Description: db.ToPtr(""), 122 + Languages: make([]string, 0), 123 + Location: db.ToPtr(""), 124 + CreatedAt: createdAt, 125 + }}, 126 + 127 + SwapRecord: cid, 128 }) 129 if err != nil { 130 + l.Error("failed to create profile record", "err", err) 131 + htmx.HxError(w, http.StatusInternalServerError, "Failed to announce profile creation, try again later") 132 + return 133 + } 134 + 135 + l.Debug("created profile record", "uri", atresp.Uri) 136 + 137 + if !o.Config.Core.Dev { 138 + err = o.Posthog.Enqueue(posthog.Capture{ 139 + DistinctId: sessData.AccountDID.String(), 140 + Event: ph.UserSignInSuccessEvent, 141 + }) 142 + if err != nil { 143 + l.Error("failed to enqueue posthog event", "err", err) 144 + } 145 + 146 + properties := posthog.NewProperties(). 147 + Set("display_name", resolved.Handle.String()). 148 + Set("language_count", 0). 149 + Set("$set_once", posthog.NewProperties(). 150 + Set("initial_did", did). 151 + Set("initial_handle", resolved.Handle.String()). 152 + Set("created_at", createdAt), 153 + ) 154 + 155 + err = o.Posthog.Enqueue(posthog.Identify{ 156 + DistinctId: did, 157 + Properties: properties, 158 + }) 159 + if err != nil { 160 + l.Error("failed to enqueue posthog identify event", "err", err) 161 + } 162 + 163 + err = o.Posthog.Enqueue(posthog.Capture{ 164 + DistinctId: did, 165 + Event: ph.ProfileRecordCreatedEvent, 166 + }) 167 + if err != nil { 168 + l.Error("failed to enqueue posthog event", "err", err) 169 + } 170 } 171 } 172
+12 -6
internal/server/oauth/oauth.go
··· 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "time" 8 ··· 11 12 13 14 15 16 17 18 19 - 20 - 21 - 22 - 23 - 24 Config *config.Config 25 JwksUri string 26 Posthog posthog.Client 27 } 28 29 - func New(config *config.Config, ph posthog.Client) (*OAuth, error) { 30 var oauthConfig oauth.ClientConfig 31 var clientUri string 32 ··· 56 SessionStore: sessStore, 57 JwksUri: jwksUri, 58 Posthog: ph, 59 }, nil 60 61 }
··· 3 import ( 4 "errors" 5 "fmt" 6 + "log/slog" 7 "net/http" 8 "time" 9 ··· 12 13 14 15 + "github.com/gorilla/sessions" 16 + "github.com/posthog/posthog-go" 17 18 + "yoten.app/internal/atproto" 19 + "yoten.app/internal/server/config" 20 + "yoten.app/internal/types" 21 + ) 22 23 24 25 26 Config *config.Config 27 JwksUri string 28 Posthog posthog.Client 29 + Logger *slog.Logger 30 + IdResolver *atproto.Resolver 31 } 32 33 + func New(config *config.Config, ph posthog.Client, idResolver *atproto.Resolver, logger *slog.Logger) (*OAuth, error) { 34 var oauthConfig oauth.ClientConfig 35 var clientUri string 36 ··· 60 SessionStore: sessStore, 61 JwksUri: jwksUri, 62 Posthog: ph, 63 + IdResolver: idResolver, 64 + Logger: logger, 65 }, nil 66 67 }
+7 -4
internal/db/db.go
··· 4 "context" 5 "database/sql" 6 "fmt" 7 "strings" 8 9 _ "github.com/mattn/go-sqlite3" ··· 11 12 type DB struct { 13 *sql.DB 14 } 15 16 type Execer interface { ··· 24 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 25 } 26 27 - func Make(dbPath string) (*DB, error) { 28 opts := []string{ 29 "_foreign_keys=1", 30 "_journal_mode=WAL", ··· 37 return nil, fmt.Errorf("failed to open db: %w", err) 38 } 39 40 - ctx := context.Background() 41 - 42 conn, err := db.Conn(ctx) 43 if err != nil { 44 return nil, err ··· 231 return nil, fmt.Errorf("failed to execute db create statement: %w", err) 232 } 233 234 - return &DB{db}, nil 235 }
··· 4 "context" 5 "database/sql" 6 "fmt" 7 + "log/slog" 8 "strings" 9 10 _ "github.com/mattn/go-sqlite3" ··· 12 13 type DB struct { 14 *sql.DB 15 + logger *slog.Logger 16 } 17 18 type Execer interface { ··· 26 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 27 } 28 29 + func Make(ctx context.Context, dbPath string, logger *slog.Logger) (*DB, error) { 30 opts := []string{ 31 "_foreign_keys=1", 32 "_journal_mode=WAL", ··· 39 return nil, fmt.Errorf("failed to open db: %w", err) 40 } 41 42 conn, err := db.Conn(ctx) 43 if err != nil { 44 return nil, err ··· 231 return nil, fmt.Errorf("failed to execute db create statement: %w", err) 232 } 233 234 + return &DB{ 235 + db, 236 + logger, 237 + }, nil 238 }
+58 -47
internal/server/handlers/study-session.go
··· 3 import ( 4 "errors" 5 "fmt" 6 - "log" 7 "net/http" 8 "strconv" 9 "time" ··· 117 } 118 119 func (h *Handler) HandleStudySessionFeed(w http.ResponseWriter, r *http.Request) { 120 isFriends, err := strconv.ParseBool(r.URL.Query().Get("friends")) 121 if err != nil { 122 - log.Println("failed to parse friends value:", err) 123 isFriends = false 124 } 125 ··· 129 } 130 page, err := strconv.ParseInt(pageStr, 10, 64) 131 if err != nil { 132 - log.Println("failed to parse page value:", err) 133 page = 1 134 } 135 if page == 0 { ··· 148 if !isFriends { 149 feed, err = db.GetStudySessionFeed(h.Db, pageSize+1, int(offset)) 150 if err != nil { 151 - log.Println("failed to get global feed:", err) 152 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 153 return 154 } 155 err = h.GetBskyProfileHydratedSessionFeed(feed) 156 if err != nil { 157 - log.Println("failed to hydrate bsky profiles:", err) 158 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 159 return 160 } 161 } else { 162 if fetchUserError != nil { 163 - log.Println("failed to get logged-in user:", err) 164 htmx.HxRedirect(w, "/login") 165 return 166 } 167 168 feed, err = db.GetFriendsStudySessionFeed(h.Db, user.Did, pageSize+1, int(offset)) 169 if err != nil { 170 - log.Println("failed to get global feed:", err) 171 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 172 return 173 } 174 err = h.GetBskyProfileHydratedSessionFeed(feed) 175 if err != nil { 176 - log.Println("failed to hydrate bsky profiles:", err) 177 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 178 return 179 } ··· 181 182 feed, err = ApplyPendingChanges(h, w, r, feed, PendingStudySessionCreation, PendingStudySessionUpdates, PendingStudySessionDeletion) 183 if err != nil { 184 - log.Printf("failed to save yoten-session after processing pending changes: %v", err) 185 } 186 187 nextPage := 0 ··· 200 } 201 202 func (h *Handler) HandleEditStudySessionPage(w http.ResponseWriter, r *http.Request) { 203 client, err := h.Oauth.AuthorizedClient(r) 204 if err != nil { 205 - log.Println("failed to get authorized client:", err) 206 htmx.HxRedirect(w, "/login") 207 return 208 } 209 210 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 211 if err != nil { 212 - log.Println("failed to get logged-in user:", err) 213 htmx.HxRedirect(w, "/login") 214 return 215 } ··· 217 rkey := chi.URLParam(r, "rkey") 218 studySession, err := db.GetStudySessionByRkey(h.Db, user.Did, rkey) 219 if err != nil { 220 - log.Println("failed to get study session from db:", err) 221 htmx.HxError(w, http.StatusInternalServerError, "Failed to update study session, try again later.") 222 return 223 } 224 225 if user.Did != studySession.Did { 226 - log.Printf("user '%s' does not own record '%s'", user.Did, studySession.Rkey) 227 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this study session.") 228 return 229 } ··· 232 case http.MethodGet: 233 userDefinedActivities, err := db.GetActivitiesByDid(h.Db, user.Did) 234 if err != nil { 235 - log.Println("failed to get user-defined activities:", err) 236 } 237 238 resources, err := db.GetResourcesByDid(h.Db, user.Did) 239 if err != nil { 240 - log.Println("failed to get user-defined resources:", err) 241 } 242 243 preDefinedActivities, err := db.GetPredefinedActivities(h.Db) 244 if err != nil { 245 - log.Println("failed to get pre-defined activities:", err) 246 } 247 248 currentResource := studySession.Resource ··· 267 268 languages, err := db.GetProfileLanguages(h.Db, user.Did) 269 if err != nil { 270 - log.Println("failed to fetch profile languages:", err) 271 } 272 273 views.EditStudySessionPage(views.EditStudySessionPageParams{ ··· 280 case http.MethodPost: 281 updatedStudySession, err := h.parseStudySessionForm(r) 282 if err != nil { 283 - log.Println("invalid study session form:", err) 284 htmx.HxError(w, http.StatusBadRequest, "Failed to update study session, ensure all data is valid.") 285 return 286 } ··· 289 updatedStudySession.CreatedAt = studySession.CreatedAt 290 291 if err := db.ValidateStudySession(updatedStudySession); err != nil { 292 - log.Println("invalid study session:", err) 293 switch { 294 case errors.Is(err, db.ErrSessionDescriptionTooLong): 295 htmx.HxError(w, http.StatusBadRequest, "Study session description cannot be more than 256 characters.") ··· 358 SwapRecord: cid, 359 }) 360 if err != nil { 361 - log.Println("failed to update study session record:", err) 362 htmx.HxError(w, http.StatusInternalServerError, "Failed to update study session, try again later.") 363 return 364 } 365 366 err = SavePendingUpdate(h, w, r, PendingStudySessionUpdates, updatedStudySession) 367 if err != nil { 368 - log.Printf("failed to save yoten-session to add pending study session updates: %v", err) 369 } 370 371 if !h.Config.Core.Dev { ··· 377 Set("rkey", rkey), 378 }) 379 if err != nil { 380 - log.Println("failed to enqueue posthog event:", err) 381 } 382 } 383 ··· 386 } 387 388 func (h *Handler) HandleNewStudySessionPage(w http.ResponseWriter, r *http.Request) { 389 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 390 if err != nil { 391 - log.Println("failed to get logged-in user:", err) 392 htmx.HxRedirect(w, "/login") 393 return 394 } 395 396 client, err := h.Oauth.AuthorizedClient(r) 397 if err != nil { 398 - log.Println("failed to get authorized client:", err) 399 htmx.HxRedirect(w, "/login") 400 return 401 } ··· 404 case http.MethodGet: 405 profile, err := db.GetProfile(h.Db, user.Did) 406 if err != nil { 407 - log.Printf("failed to find %s in db: %s", user.Did, err) 408 htmx.HxError(w, http.StatusNotFound, "Failed to find user.") 409 return 410 } 411 412 userDefinedActivities, err := db.GetActivitiesByDid(h.Db, user.Did) 413 if err != nil { 414 - log.Println("failed to get user-defined activities:", err) 415 } 416 preDefinedActivities, err := db.GetPredefinedActivities(h.Db) 417 if err != nil { 418 - log.Println("failed to get pre-defined activities:", err) 419 } 420 activeActivities := utils.Filter(userDefinedActivities, func(activity db.Activity) bool { 421 return activity.Status != db.Deleted ··· 424 425 resources, err := db.GetResourcesByDid(h.Db, user.Did) 426 if err != nil { 427 - log.Println("failed to get user-defined resources:", err) 428 } 429 activeResources := utils.Filter(resources, func(resource db.Resource) bool { 430 return resource.Status != db.Deleted ··· 440 case http.MethodPost: 441 newStudySession, err := h.parseStudySessionForm(r) 442 if err != nil { 443 - log.Println("invalid study session form:", err) 444 htmx.HxError(w, http.StatusBadRequest, "Failed to update study session, ensure all data is valid.") 445 return 446 } ··· 459 } 460 461 if err := db.ValidateStudySession(newStudySession); err != nil { 462 - log.Println("invalid study session:", err) 463 switch { 464 case errors.Is(err, db.ErrSessionDescriptionTooLong): 465 htmx.HxError(w, http.StatusBadRequest, "Study session description cannot be more than 256 characters.") ··· 521 }, 522 }) 523 if err != nil { 524 - log.Println("failed to create study session record:", err) 525 htmx.HxError(w, http.StatusInternalServerError, "Failed to create study session, try again later.") 526 return 527 } 528 529 err = SavePendingCreate(h, w, r, PendingStudySessionCreation, newStudySession) 530 if err != nil { 531 - log.Printf("failed to save yoten-session to add pending study session creation: %v", err) 532 } 533 534 if !h.Config.Core.Dev { ··· 545 Set("date_is_today", newStudySession.Date.Truncate(24*time.Hour).Equal(time.Now().UTC().In(loc).Truncate(24*time.Hour))), 546 }) 547 if err != nil { 548 - log.Println("failed to enqueue posthog event:", err) 549 } 550 } 551 ··· 554 } 555 556 func (h *Handler) HandleDeleteStudySession(w http.ResponseWriter, r *http.Request) { 557 user := h.Oauth.GetUser(r) 558 if user == nil { 559 - log.Println("failed to get logged-in user") 560 htmx.HxRedirect(w, "/login") 561 return 562 } 563 564 client, err := h.Oauth.AuthorizedClient(r) 565 if err != nil { 566 - log.Println("failed to get authorized client:", err) 567 htmx.HxError(w, http.StatusUnauthorized, "Failed to delete study session, try again later.") 568 return 569 } ··· 572 case http.MethodDelete: 573 err := r.ParseForm() 574 if err != nil { 575 - log.Println("failed to parse study session delete form:", err) 576 htmx.HxError(w, http.StatusBadRequest, "Failed to delete study session, try again later.") 577 return 578 } ··· 580 rkey := chi.URLParam(r, "rkey") 581 studySession, err := db.GetStudySessionByRkey(h.Db, user.Did, rkey) 582 if err != nil { 583 - log.Println("failed to get study session from db:", err) 584 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete study session, try again later.") 585 return 586 } 587 588 if user.Did != studySession.Did { 589 - log.Println("failed to delete study session: user does not own record") 590 htmx.HxError(w, http.StatusUnauthorized, "Failed to delete study session, try again later.") 591 return 592 } ··· 597 Rkey: rkey, 598 }) 599 if err != nil { 600 - log.Println("failed to delete study session from PDS:", err) 601 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete study session, try again later.") 602 return 603 } 604 605 err = SavePendingDelete(h, w, r, PendingStudySessionDeletion, studySession) 606 if err != nil { 607 - log.Printf("failed to save yoten-session to add pending study session deletion: %v", err) 608 } 609 610 if !h.Config.Core.Dev { ··· 617 Set("session_age_seconds", time.Since(studySession.CreatedAt).Seconds()), 618 }) 619 if err != nil { 620 - log.Println("failed to enqueue posthog event:", err) 621 } 622 } 623 ··· 644 } 645 646 func (h *Handler) HandleStudySessionPage(w http.ResponseWriter, r *http.Request) { 647 user, _ := bsky.GetUserWithBskyProfile(h.Oauth, r) 648 didOrHandle := chi.URLParam(r, "user") 649 if didOrHandle == "" { ··· 661 662 studySession, err := db.GetStudySessionByRkey(h.Db, ident.DID.String(), rkey) 663 if err != nil { 664 - log.Println("failed to retrieve study session:", err) 665 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve study session, try again later.") 666 return 667 } 668 669 bskyProfile, err := bsky.GetBskyProfile(ident.DID.String()) 670 if err != nil { 671 - log.Println("failed to retrieve bsky profile for study session:", err) 672 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve bsky profile, try again later.") 673 return 674 } 675 676 profile, err := db.GetProfile(h.Db, ident.DID.String()) 677 if err != nil { 678 - log.Println("failed to retrieve profile for study session:", err) 679 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve profile, try again later.") 680 return 681 } ··· 698 } 699 700 func (h *Handler) HandleStudySessionPageCommentFeed(w http.ResponseWriter, r *http.Request) { 701 user, _ := bsky.GetUserWithBskyProfile(h.Oauth, r) 702 703 didOrHandle := chi.URLParam(r, "user") ··· 721 } 722 page, err := strconv.ParseInt(pageStr, 10, 64) 723 if err != nil { 724 - log.Println("failed to parse page value:", err) 725 page = 1 726 } 727 if page == 0 { ··· 733 734 commentFeed, err := db.GetCommentsForSession(h.Db, studySessionUri.String(), pageSize+1, int(offset)) 735 if err != nil { 736 - log.Println("failed to get comment feed:", err) 737 htmx.HxError(w, http.StatusInternalServerError, "Failed to get comment feed, try again later.") 738 return 739 } ··· 769 770 populatedCommentFeed, err := h.BuildCommentFeed(finalFeed) 771 if err != nil { 772 - log.Println("failed to populate comment feed:", err) 773 htmx.HxError(w, http.StatusInternalServerError, "Failed to get comment feed, try again later.") 774 return 775 }
··· 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "strconv" 8 "time" ··· 116 } 117 118 func (h *Handler) HandleStudySessionFeed(w http.ResponseWriter, r *http.Request) { 119 + l := h.Logger.With("handler", "HandleStudySessionFeed") 120 + 121 isFriends, err := strconv.ParseBool(r.URL.Query().Get("friends")) 122 if err != nil { 123 + l.Error("failed to parse friends value", "err", err) 124 isFriends = false 125 } 126 ··· 130 } 131 page, err := strconv.ParseInt(pageStr, 10, 64) 132 if err != nil { 133 + l.Error("failed to parse page value", "err", err) 134 page = 1 135 } 136 if page == 0 { ··· 149 if !isFriends { 150 feed, err = db.GetStudySessionFeed(h.Db, pageSize+1, int(offset)) 151 if err != nil { 152 + l.Error("failed to get global feed", "err", err) 153 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 154 return 155 } 156 err = h.GetBskyProfileHydratedSessionFeed(feed) 157 if err != nil { 158 + l.Error("failed to hydrate bsky profiles", "err", err) 159 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 160 return 161 } 162 } else { 163 if fetchUserError != nil { 164 + l.Error("failed to get logged-in user", "err", err) 165 htmx.HxRedirect(w, "/login") 166 return 167 } 168 169 feed, err = db.GetFriendsStudySessionFeed(h.Db, user.Did, pageSize+1, int(offset)) 170 if err != nil { 171 + l.Error("failed to get global feed", "err", err) 172 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 173 return 174 } 175 err = h.GetBskyProfileHydratedSessionFeed(feed) 176 if err != nil { 177 + l.Error("failed to hydrate bsky profiles", "err", err) 178 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 179 return 180 } ··· 182 183 feed, err = ApplyPendingChanges(h, w, r, feed, PendingStudySessionCreation, PendingStudySessionUpdates, PendingStudySessionDeletion) 184 if err != nil { 185 + l.Error("failed to save yoten-session after processing pending changes", "err", err) 186 } 187 188 nextPage := 0 ··· 201 } 202 203 func (h *Handler) HandleEditStudySessionPage(w http.ResponseWriter, r *http.Request) { 204 + l := h.Logger.With("handler", "HandleEditStudySessionPage") 205 + 206 client, err := h.Oauth.AuthorizedClient(r) 207 if err != nil { 208 + l.Error("failed to get authorized client", "err", err) 209 htmx.HxRedirect(w, "/login") 210 return 211 } 212 213 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 214 if err != nil { 215 + l.Error("failed to get logged-in user", "err", err) 216 htmx.HxRedirect(w, "/login") 217 return 218 } ··· 220 rkey := chi.URLParam(r, "rkey") 221 studySession, err := db.GetStudySessionByRkey(h.Db, user.Did, rkey) 222 if err != nil { 223 + l.Error("failed to get study session from db", "err", err) 224 htmx.HxError(w, http.StatusInternalServerError, "Failed to update study session, try again later.") 225 return 226 } 227 228 if user.Did != studySession.Did { 229 + l.Error("user does not own record", "did", user.Did, "sessionDid", studySession.Did) 230 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this study session.") 231 return 232 } ··· 235 case http.MethodGet: 236 userDefinedActivities, err := db.GetActivitiesByDid(h.Db, user.Did) 237 if err != nil { 238 + l.Error("failed to get user-defined activities", "err", err) 239 } 240 241 resources, err := db.GetResourcesByDid(h.Db, user.Did) 242 if err != nil { 243 + l.Error("failed to get user-defined resources", "err", err) 244 } 245 246 preDefinedActivities, err := db.GetPredefinedActivities(h.Db) 247 if err != nil { 248 + l.Error("failed to get pre-defined activities", "err", err) 249 } 250 251 currentResource := studySession.Resource ··· 270 271 languages, err := db.GetProfileLanguages(h.Db, user.Did) 272 if err != nil { 273 + l.Error("failed to fetch profile languages", "err", err) 274 } 275 276 views.EditStudySessionPage(views.EditStudySessionPageParams{ ··· 283 case http.MethodPost: 284 updatedStudySession, err := h.parseStudySessionForm(r) 285 if err != nil { 286 + l.Error("invalid study session form", "err", err) 287 htmx.HxError(w, http.StatusBadRequest, "Failed to update study session, ensure all data is valid.") 288 return 289 } ··· 292 updatedStudySession.CreatedAt = studySession.CreatedAt 293 294 if err := db.ValidateStudySession(updatedStudySession); err != nil { 295 + l.Error("invalid study session", "err", err) 296 switch { 297 case errors.Is(err, db.ErrSessionDescriptionTooLong): 298 htmx.HxError(w, http.StatusBadRequest, "Study session description cannot be more than 256 characters.") ··· 361 SwapRecord: cid, 362 }) 363 if err != nil { 364 + l.Error("failed to update study session record", "err", err) 365 htmx.HxError(w, http.StatusInternalServerError, "Failed to update study session, try again later.") 366 return 367 } 368 369 err = SavePendingUpdate(h, w, r, PendingStudySessionUpdates, updatedStudySession) 370 if err != nil { 371 + l.Error("failed to save yoten-session to add pending study session updates", "err", err) 372 } 373 374 if !h.Config.Core.Dev { ··· 380 Set("rkey", rkey), 381 }) 382 if err != nil { 383 + l.Error("failed to enqueue posthog event", "err", err) 384 } 385 } 386 ··· 389 } 390 391 func (h *Handler) HandleNewStudySessionPage(w http.ResponseWriter, r *http.Request) { 392 + l := h.Logger.With("handler", "HandleNewStudySessionPage") 393 + 394 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 395 if err != nil { 396 + l.Error("failed to get logged-in user", "err", err) 397 htmx.HxRedirect(w, "/login") 398 return 399 } 400 401 client, err := h.Oauth.AuthorizedClient(r) 402 if err != nil { 403 + l.Error("failed to get authorized client", "err", err) 404 htmx.HxRedirect(w, "/login") 405 return 406 } ··· 409 case http.MethodGet: 410 profile, err := db.GetProfile(h.Db, user.Did) 411 if err != nil { 412 + l.Error("failed to find user in db", "did", user.Did, "err", err) 413 htmx.HxError(w, http.StatusNotFound, "Failed to find user.") 414 return 415 } 416 417 userDefinedActivities, err := db.GetActivitiesByDid(h.Db, user.Did) 418 if err != nil { 419 + l.Error("failed to get user-defined activities", "err", err) 420 } 421 preDefinedActivities, err := db.GetPredefinedActivities(h.Db) 422 if err != nil { 423 + l.Error("failed to get pre-defined activities", "err", err) 424 } 425 activeActivities := utils.Filter(userDefinedActivities, func(activity db.Activity) bool { 426 return activity.Status != db.Deleted ··· 429 430 resources, err := db.GetResourcesByDid(h.Db, user.Did) 431 if err != nil { 432 + l.Error("failed to get user-defined resources", "err", err) 433 } 434 activeResources := utils.Filter(resources, func(resource db.Resource) bool { 435 return resource.Status != db.Deleted ··· 445 case http.MethodPost: 446 newStudySession, err := h.parseStudySessionForm(r) 447 if err != nil { 448 + l.Error("invalid study session form", "err", err) 449 htmx.HxError(w, http.StatusBadRequest, "Failed to update study session, ensure all data is valid.") 450 return 451 } ··· 464 } 465 466 if err := db.ValidateStudySession(newStudySession); err != nil { 467 + l.Error("invalid study session", "err", err) 468 switch { 469 case errors.Is(err, db.ErrSessionDescriptionTooLong): 470 htmx.HxError(w, http.StatusBadRequest, "Study session description cannot be more than 256 characters.") ··· 526 }, 527 }) 528 if err != nil { 529 + l.Error("failed to create study session record", "err", err) 530 htmx.HxError(w, http.StatusInternalServerError, "Failed to create study session, try again later.") 531 return 532 } 533 534 err = SavePendingCreate(h, w, r, PendingStudySessionCreation, newStudySession) 535 if err != nil { 536 + l.Error("failed to save yoten-session to add pending study session creation", "err", err) 537 } 538 539 if !h.Config.Core.Dev { ··· 550 Set("date_is_today", newStudySession.Date.Truncate(24*time.Hour).Equal(time.Now().UTC().In(loc).Truncate(24*time.Hour))), 551 }) 552 if err != nil { 553 + l.Error("failed to enqueue posthog event", "err", err) 554 } 555 } 556 ··· 559 } 560 561 func (h *Handler) HandleDeleteStudySession(w http.ResponseWriter, r *http.Request) { 562 + l := h.Logger.With("handler", "HandleDeleteStudySession") 563 + 564 user := h.Oauth.GetUser(r) 565 if user == nil { 566 + l.Error("failed to get logged-in user") 567 htmx.HxRedirect(w, "/login") 568 return 569 } 570 571 client, err := h.Oauth.AuthorizedClient(r) 572 if err != nil { 573 + l.Error("failed to get authorized client", "err", err) 574 htmx.HxError(w, http.StatusUnauthorized, "Failed to delete study session, try again later.") 575 return 576 } ··· 579 case http.MethodDelete: 580 err := r.ParseForm() 581 if err != nil { 582 + l.Error("failed to parse study session delete form", "err", err) 583 htmx.HxError(w, http.StatusBadRequest, "Failed to delete study session, try again later.") 584 return 585 } ··· 587 rkey := chi.URLParam(r, "rkey") 588 studySession, err := db.GetStudySessionByRkey(h.Db, user.Did, rkey) 589 if err != nil { 590 + l.Error("failed to get study session from db", "err", err) 591 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete study session, try again later.") 592 return 593 } 594 595 if user.Did != studySession.Did { 596 + l.Error("user does not own record", "did", user.Did, "sessionDid", studySession.Did) 597 htmx.HxError(w, http.StatusUnauthorized, "Failed to delete study session, try again later.") 598 return 599 } ··· 604 Rkey: rkey, 605 }) 606 if err != nil { 607 + l.Error("failed to delete study session from PDS", "err", err) 608 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete study session, try again later.") 609 return 610 } 611 612 err = SavePendingDelete(h, w, r, PendingStudySessionDeletion, studySession) 613 if err != nil { 614 + l.Error("failed to save yoten-session to add pending study session deletion", "err", err) 615 } 616 617 if !h.Config.Core.Dev { ··· 624 Set("session_age_seconds", time.Since(studySession.CreatedAt).Seconds()), 625 }) 626 if err != nil { 627 + l.Error("failed to enqueue posthog event", "err", err) 628 } 629 } 630 ··· 651 } 652 653 func (h *Handler) HandleStudySessionPage(w http.ResponseWriter, r *http.Request) { 654 + l := h.Logger.With("handler", "HandleStudySessionPage") 655 + 656 user, _ := bsky.GetUserWithBskyProfile(h.Oauth, r) 657 didOrHandle := chi.URLParam(r, "user") 658 if didOrHandle == "" { ··· 670 671 studySession, err := db.GetStudySessionByRkey(h.Db, ident.DID.String(), rkey) 672 if err != nil { 673 + l.Error("failed to retrieve study session", "err", err) 674 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve study session, try again later.") 675 return 676 } 677 678 bskyProfile, err := bsky.GetBskyProfile(ident.DID.String()) 679 if err != nil { 680 + l.Error("failed to retrieve bsky profile for study session", "err", err) 681 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve bsky profile, try again later.") 682 return 683 } 684 685 profile, err := db.GetProfile(h.Db, ident.DID.String()) 686 if err != nil { 687 + l.Error("failed to retrieve profile for study session", "err", err) 688 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve profile, try again later.") 689 return 690 } ··· 707 } 708 709 func (h *Handler) HandleStudySessionPageCommentFeed(w http.ResponseWriter, r *http.Request) { 710 + l := h.Logger.With("handler", "HandleStudySessionPageCommentFeed") 711 + 712 user, _ := bsky.GetUserWithBskyProfile(h.Oauth, r) 713 714 didOrHandle := chi.URLParam(r, "user") ··· 732 } 733 page, err := strconv.ParseInt(pageStr, 10, 64) 734 if err != nil { 735 + l.Error("failed to parse page value", "err", err) 736 page = 1 737 } 738 if page == 0 { ··· 744 745 commentFeed, err := db.GetCommentsForSession(h.Db, studySessionUri.String(), pageSize+1, int(offset)) 746 if err != nil { 747 + l.Error("failed to get comment feed", "err", err) 748 htmx.HxError(w, http.StatusInternalServerError, "Failed to get comment feed, try again later.") 749 return 750 } ··· 780 781 populatedCommentFeed, err := h.BuildCommentFeed(finalFeed) 782 if err != nil { 783 + l.Error("failed to populate comment feed", "err", err) 784 htmx.HxError(w, http.StatusInternalServerError, "Failed to get comment feed, try again later.") 785 return 786 }
+31 -26
internal/server/handlers/resource.go
··· 3 import ( 4 "errors" 5 "fmt" 6 - "log" 7 "net/http" 8 "time" 9 ··· 63 } 64 65 func (h *Handler) HandleNewResourcePage(w http.ResponseWriter, r *http.Request) { 66 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 67 if err != nil { 68 - log.Println("failed to get logged-in user:", err) 69 htmx.HxRedirect(w, "/login") 70 return 71 } ··· 79 case http.MethodPost: 80 client, err := h.Oauth.AuthorizedClient(r) 81 if err != nil { 82 - log.Println("failed to get authorized client:", err) 83 htmx.HxRedirect(w, "/login") 84 return 85 } 86 87 newResource, err := parseResourceForm(r) 88 if err != nil { 89 - log.Println("invalid resource form:", err) 90 htmx.HxError(w, http.StatusBadRequest, "Failed to create resource, ensure all fields contain valid data.") 91 return 92 } ··· 95 newResource.CreatedAt = time.Now().UTC() 96 97 if err := db.ValidateResource(newResource); err != nil { 98 - log.Println("invalid resource definition:", err) 99 switch { 100 case errors.Is(err, db.ErrResourceTitleEmpty): 101 htmx.HxError(w, http.StatusBadRequest, "Resource must have a title.") ··· 118 if newResource.Link != nil { 119 linkCheckResult := google.CheckResourceLinkSafety(*newResource.Link) 120 if linkCheckResult.Err != nil { 121 - log.Println("invalid resource definition:", linkCheckResult.Err) 122 switch { 123 case errors.Is(linkCheckResult.Err, google.ErrResourceLinkSketchy): 124 if !h.Config.Core.Dev { ··· 186 }, 187 }) 188 if err != nil { 189 - log.Println("failed to create resource record:", err) 190 htmx.HxError(w, http.StatusInternalServerError, "Failed to create resource, try again later.") 191 return 192 } 193 194 err = SavePendingCreate(h, w, r, PendingResourceCreation, newResource) 195 if err != nil { 196 - log.Printf("failed to save yoten-session to add pending resource creation: %v", err) 197 } 198 199 if !h.Config.Core.Dev { ··· 209 Set("link_provided", newResource.Link != nil), 210 }) 211 if err != nil { 212 - log.Println("failed to enqueue posthog event:", err) 213 } 214 } 215 ··· 218 } 219 220 func (h *Handler) HandleDeleteResource(w http.ResponseWriter, r *http.Request) { 221 user := h.Oauth.GetUser(r) 222 if user == nil { 223 - log.Println("failed to get logged-in user") 224 htmx.HxRedirect(w, "/login") 225 return 226 } 227 client, err := h.Oauth.AuthorizedClient(r) 228 if err != nil { 229 - log.Println("failed to get authorized client:", err) 230 htmx.HxError(w, http.StatusUnauthorized, "Failed to delete resource, try again later.") 231 return 232 } ··· 236 rkey := chi.URLParam(r, "rkey") 237 resource, err := db.GetResourceByRkey(h.Db, user.Did, rkey) 238 if err != nil { 239 - log.Println("failed to get resource from db:", err) 240 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete resource, try again later.") 241 return 242 } 243 244 if user.Did != resource.Did { 245 - log.Printf("user '%s' does not own record '%s'", user.Did, rkey) 246 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to delete this resource.") 247 return 248 } ··· 253 Rkey: resource.Rkey, 254 }) 255 if err != nil { 256 - log.Println("failed to delete resource from PDS:", err) 257 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete resource, try again later.") 258 return 259 } 260 261 err = SavePendingDelete(h, w, r, PendingResourceDeletion, resource) 262 if err != nil { 263 - log.Printf("failed to save yoten-session to add pending resource deletion: %v", err) 264 } 265 266 if !h.Config.Core.Dev { ··· 272 Set("resource_type", resource.Type), 273 }) 274 if err != nil { 275 - log.Println("failed to enqueue posthog event:", err) 276 } 277 } 278 ··· 281 } 282 283 func (h *Handler) HandleEditResourcePage(w http.ResponseWriter, r *http.Request) { 284 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 285 if err != nil { 286 - log.Println("failed to get logged-in user:", err) 287 htmx.HxRedirect(w, "/login") 288 return 289 } ··· 291 rkey := chi.URLParam(r, "rkey") 292 resource, err := db.GetResourceByRkey(h.Db, user.Did, rkey) 293 if err != nil { 294 - log.Println("failed to get resource from db:", err) 295 htmx.HxError(w, http.StatusInternalServerError, "Failed to update resource, try again later.") 296 return 297 } 298 299 if user.Did != resource.Did { 300 - log.Printf("user '%s' does not own record '%s'", user.Did, rkey) 301 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this resource.") 302 return 303 } ··· 312 case http.MethodPost: 313 client, err := h.Oauth.AuthorizedClient(r) 314 if err != nil { 315 - log.Println("failed to get authorized client:", err) 316 htmx.HxRedirect(w, "/login") 317 return 318 } 319 320 updatedResource, err := parseResourceForm(r) 321 if err != nil { 322 - log.Println("invalid resource form:", err) 323 htmx.HxError(w, http.StatusBadRequest, "Failed to create resource, ensure all fields contain valid data.") 324 return 325 } ··· 328 updatedResource.CreatedAt = resource.CreatedAt 329 330 if err := db.ValidateResource(updatedResource); err != nil { 331 - log.Println("invalid resource definition:", err) 332 switch { 333 case errors.Is(err, db.ErrResourceTitleEmpty): 334 htmx.HxError(w, http.StatusBadRequest, "Resource must have a title.") ··· 352 if updatedResource.Link != nil && (resource.Link == nil || *updatedResource.Link != *resource.Link) { 353 linkCheckResult := google.CheckResourceLinkSafety(*updatedResource.Link) 354 if linkCheckResult.Err != nil { 355 - log.Println("invalid resource definition:", linkCheckResult.Err) 356 switch { 357 case errors.Is(linkCheckResult.Err, google.ErrResourceLinkSketchy): 358 if !h.Config.Core.Dev { ··· 427 SwapRecord: cid, 428 }) 429 if err != nil { 430 - log.Println("failed to update resource record:", err) 431 htmx.HxError(w, http.StatusInternalServerError, "Failed to update resource, try again later.") 432 return 433 } 434 435 err = SavePendingUpdate(h, w, r, PendingResourceUpdates, updatedResource) 436 if err != nil { 437 - log.Printf("failed to save yoten-session to add pending resource updates: %v", err) 438 } 439 440 if !h.Config.Core.Dev { ··· 450 Set("link_provided", updatedResource.Link != nil), 451 }) 452 if err != nil { 453 - log.Println("failed to enqueue posthog event:", err) 454 } 455 } 456
··· 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "time" 8 ··· 62 } 63 64 func (h *Handler) HandleNewResourcePage(w http.ResponseWriter, r *http.Request) { 65 + l := h.Logger.With("handler", "HandleNewResourcePage") 66 + 67 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 68 if err != nil { 69 + l.Error("failed to get logged-in user", "err", err) 70 htmx.HxRedirect(w, "/login") 71 return 72 } ··· 80 case http.MethodPost: 81 client, err := h.Oauth.AuthorizedClient(r) 82 if err != nil { 83 + l.Error("failed to get authorized client", "err", err) 84 htmx.HxRedirect(w, "/login") 85 return 86 } 87 88 newResource, err := parseResourceForm(r) 89 if err != nil { 90 + l.Error("invalid resource form", "err", err) 91 htmx.HxError(w, http.StatusBadRequest, "Failed to create resource, ensure all fields contain valid data.") 92 return 93 } ··· 96 newResource.CreatedAt = time.Now().UTC() 97 98 if err := db.ValidateResource(newResource); err != nil { 99 + l.Error("invalid resource definition", "err", err) 100 switch { 101 case errors.Is(err, db.ErrResourceTitleEmpty): 102 htmx.HxError(w, http.StatusBadRequest, "Resource must have a title.") ··· 119 if newResource.Link != nil { 120 linkCheckResult := google.CheckResourceLinkSafety(*newResource.Link) 121 if linkCheckResult.Err != nil { 122 + l.Error("invalid resource definition", "err", linkCheckResult.Err) 123 switch { 124 case errors.Is(linkCheckResult.Err, google.ErrResourceLinkSketchy): 125 if !h.Config.Core.Dev { ··· 187 }, 188 }) 189 if err != nil { 190 + l.Error("failed to create resource record", "err", err) 191 htmx.HxError(w, http.StatusInternalServerError, "Failed to create resource, try again later.") 192 return 193 } 194 195 err = SavePendingCreate(h, w, r, PendingResourceCreation, newResource) 196 if err != nil { 197 + l.Error("failed to save yoten-session to add pending resource creation", "err", err) 198 } 199 200 if !h.Config.Core.Dev { ··· 210 Set("link_provided", newResource.Link != nil), 211 }) 212 if err != nil { 213 + l.Error("failed to enqueue posthog event", "err", err) 214 } 215 } 216 ··· 219 } 220 221 func (h *Handler) HandleDeleteResource(w http.ResponseWriter, r *http.Request) { 222 + l := h.Logger.With("handler", "HandleDeleteResource") 223 + 224 user := h.Oauth.GetUser(r) 225 if user == nil { 226 + l.Error("failed to get logged-in user") 227 htmx.HxRedirect(w, "/login") 228 return 229 } 230 client, err := h.Oauth.AuthorizedClient(r) 231 if err != nil { 232 + l.Error("failed to get authorized client", "err", err) 233 htmx.HxError(w, http.StatusUnauthorized, "Failed to delete resource, try again later.") 234 return 235 } ··· 239 rkey := chi.URLParam(r, "rkey") 240 resource, err := db.GetResourceByRkey(h.Db, user.Did, rkey) 241 if err != nil { 242 + l.Error("failed to get resource from db", "err", err) 243 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete resource, try again later.") 244 return 245 } 246 247 if user.Did != resource.Did { 248 + l.Error("user does not own record", "did", user.Did, "resourceDid", resource.Did) 249 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to delete this resource.") 250 return 251 } ··· 256 Rkey: resource.Rkey, 257 }) 258 if err != nil { 259 + l.Error("failed to delete resource from PDS", "err", err) 260 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete resource, try again later.") 261 return 262 } 263 264 err = SavePendingDelete(h, w, r, PendingResourceDeletion, resource) 265 if err != nil { 266 + l.Error("failed to save yoten-session to add pending resource deletion", "err", err) 267 } 268 269 if !h.Config.Core.Dev { ··· 275 Set("resource_type", resource.Type), 276 }) 277 if err != nil { 278 + l.Error("failed to enqueue posthog event", "err", err) 279 } 280 } 281 ··· 284 } 285 286 func (h *Handler) HandleEditResourcePage(w http.ResponseWriter, r *http.Request) { 287 + l := h.Logger.With("handler", "HandleEditResourcePage") 288 + 289 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 290 if err != nil { 291 + l.Error("failed to get logged-in user", "err", err) 292 htmx.HxRedirect(w, "/login") 293 return 294 } ··· 296 rkey := chi.URLParam(r, "rkey") 297 resource, err := db.GetResourceByRkey(h.Db, user.Did, rkey) 298 if err != nil { 299 + l.Error("failed to get resource from db", "err", err) 300 htmx.HxError(w, http.StatusInternalServerError, "Failed to update resource, try again later.") 301 return 302 } 303 304 if user.Did != resource.Did { 305 + l.Error("user does not own record", "did", user.Did, "resourceDid", resource.Did) 306 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this resource.") 307 return 308 } ··· 317 case http.MethodPost: 318 client, err := h.Oauth.AuthorizedClient(r) 319 if err != nil { 320 + l.Error("failed to get authorized client", "err", err) 321 htmx.HxRedirect(w, "/login") 322 return 323 } 324 325 updatedResource, err := parseResourceForm(r) 326 if err != nil { 327 + l.Error("invalid resource form", "err", err) 328 htmx.HxError(w, http.StatusBadRequest, "Failed to create resource, ensure all fields contain valid data.") 329 return 330 } ··· 333 updatedResource.CreatedAt = resource.CreatedAt 334 335 if err := db.ValidateResource(updatedResource); err != nil { 336 + l.Error("invalid resource definition", "err", err) 337 switch { 338 case errors.Is(err, db.ErrResourceTitleEmpty): 339 htmx.HxError(w, http.StatusBadRequest, "Resource must have a title.") ··· 357 if updatedResource.Link != nil && (resource.Link == nil || *updatedResource.Link != *resource.Link) { 358 linkCheckResult := google.CheckResourceLinkSafety(*updatedResource.Link) 359 if linkCheckResult.Err != nil { 360 + l.Error("invalid resource link", "link", resource.Link, "threatType", linkCheckResult.ThreatType, "err", linkCheckResult.Err) 361 switch { 362 case errors.Is(linkCheckResult.Err, google.ErrResourceLinkSketchy): 363 if !h.Config.Core.Dev { ··· 432 SwapRecord: cid, 433 }) 434 if err != nil { 435 + l.Error("failed to update resource record", "err", err) 436 htmx.HxError(w, http.StatusInternalServerError, "Failed to update resource, try again later.") 437 return 438 } 439 440 err = SavePendingUpdate(h, w, r, PendingResourceUpdates, updatedResource) 441 if err != nil { 442 + l.Error("failed to save yoten-session to add pending resource updates", "err", err) 443 } 444 445 if !h.Config.Core.Dev { ··· 455 Set("link_provided", updatedResource.Link != nil), 456 }) 457 if err != nil { 458 + l.Error("failed to enqueue posthog event", "err", err) 459 } 460 } 461
+36 -27
internal/server/handlers/comment.go
··· 1 package handlers 2 3 import ( 4 - "log" 5 "net/http" 6 "strings" 7 "time" ··· 23 ) 24 25 func (h *Handler) HandleNewComment(w http.ResponseWriter, r *http.Request) { 26 client, err := h.Oauth.AuthorizedClient(r) 27 if err != nil { 28 - log.Println("failed to get authorized client:", err) 29 htmx.HxRedirect(w, "/login") 30 return 31 } 32 33 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 34 if err != nil { 35 - log.Println("failed to get logged-in user:", err) 36 htmx.HxRedirect(w, "/login") 37 return 38 } 39 40 profile, err := db.GetProfile(h.Db, user.Did) 41 if err != nil { 42 - log.Println("failed to get logged-in user:", err) 43 htmx.HxRedirect(w, "/login") 44 return 45 } 46 47 err = r.ParseForm() 48 if err != nil { 49 - log.Println("invalid comment form:", err) 50 htmx.HxError(w, http.StatusBadRequest, "Unable to process comment, please try again later.") 51 return 52 } 53 54 commentBody := r.FormValue("comment") 55 if len(strings.TrimSpace(commentBody)) == 0 { 56 - log.Println("invalid comment form: missing comment body") 57 htmx.HxError(w, http.StatusBadRequest, "Comment cannot be empty.") 58 return 59 } 60 61 studySessionUri := r.FormValue("study_session_uri") 62 if len(studySessionUri) == 0 { 63 - log.Println("invalid comment form: missing study session Uri") 64 htmx.HxError(w, http.StatusBadRequest, "Unable to create comment, please try again later.") 65 return 66 } ··· 100 }, 101 }) 102 if err != nil { 103 - log.Println("failed to create comment record:", err) 104 htmx.HxError(w, http.StatusInternalServerError, "Failed to create comment, try again later.") 105 return 106 } ··· 121 122 err = h.Posthog.Enqueue(event) 123 if err != nil { 124 - log.Println("failed to enqueue posthog event:", err) 125 } 126 } 127 ··· 153 } 154 155 func (h *Handler) HandleDeleteComment(w http.ResponseWriter, r *http.Request) { 156 user := h.Oauth.GetUser(r) 157 if user == nil { 158 - log.Println("failed to get logged-in user") 159 htmx.HxRedirect(w, "/login") 160 return 161 } 162 client, err := h.Oauth.AuthorizedClient(r) 163 if err != nil { 164 - log.Println("failed to get authorized client:", err) 165 htmx.HxRedirect(w, "/login") 166 return 167 } ··· 171 rkey := chi.URLParam(r, "rkey") 172 comment, err := db.GetCommentByRkey(h.Db, user.Did, rkey) 173 if err != nil { 174 - log.Println("failed to get comment from db:", err) 175 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete comment, try again later.") 176 return 177 } 178 179 if user.Did != comment.Did { 180 - log.Printf("user '%s' does not own record '%s'", user.Did, rkey) 181 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to delete this comment.") 182 return 183 } ··· 188 Rkey: comment.Rkey, 189 }) 190 if err != nil { 191 - log.Println("failed to delete comment from PDS:", err) 192 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete comment, try again later.") 193 return 194 } ··· 209 210 err = h.Posthog.Enqueue(event) 211 if err != nil { 212 - log.Println("failed to enqueue posthog event:", err) 213 } 214 } 215 ··· 218 } 219 220 func (h *Handler) HandleEditCommentPage(w http.ResponseWriter, r *http.Request) { 221 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 222 if err != nil { 223 - log.Println("failed to get logged-in user:", err) 224 htmx.HxRedirect(w, "/login") 225 return 226 } ··· 228 rkey := chi.URLParam(r, "rkey") 229 comment, err := db.GetCommentByRkey(h.Db, user.Did, rkey) 230 if err != nil { 231 - log.Println("failed to get comment from db:", err) 232 htmx.HxError(w, http.StatusInternalServerError, "Failed to update comment, try again later.") 233 return 234 } 235 236 if user.Did != comment.Did { 237 - log.Printf("user '%s' does not own record '%s'", user.Did, rkey) 238 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this comment.") 239 return 240 } ··· 245 case http.MethodPost: 246 client, err := h.Oauth.AuthorizedClient(r) 247 if err != nil { 248 - log.Println("failed to get authorized client:", err) 249 htmx.HxRedirect(w, "/login") 250 return 251 } 252 253 err = r.ParseForm() 254 if err != nil { 255 - log.Println("invalid comment form:", err) 256 htmx.HxError(w, http.StatusBadRequest, "Unable to process comment, please try again later.") 257 return 258 } 259 260 commentBody := r.FormValue("comment") 261 if len(strings.TrimSpace(commentBody)) == 0 { 262 - log.Println("invalid comment form: missing comment body") 263 htmx.HxError(w, http.StatusBadRequest, "Comment cannot be empty.") 264 return 265 } ··· 303 SwapRecord: cid, 304 }) 305 if err != nil { 306 - log.Println("failed to update study session record:", err) 307 htmx.HxError(w, http.StatusInternalServerError, "Failed to update comment, try again later.") 308 return 309 } ··· 324 325 err = h.Posthog.Enqueue(event) 326 if err != nil { 327 - log.Println("failed to enqueue posthog event:", err) 328 } 329 } 330 ··· 387 } 388 389 func (h *Handler) HandleReply(w http.ResponseWriter, r *http.Request) { 390 user := h.Oauth.GetUser(r) 391 if user == nil { 392 - log.Println("failed to get logged-in user") 393 htmx.HxRedirect(w, "/login") 394 return 395 } ··· 397 studySessionUri := r.URL.Query().Get("root") 398 parentCommentUri := r.URL.Query().Get("parent") 399 if len(studySessionUri) == 0 || len(parentCommentUri) == 0 { 400 - log.Println("invalid reply form: study session uri or parent comment uri is empty") 401 htmx.HxError(w, http.StatusBadRequest, "Unable to process comment, please try again later.") 402 return 403 } ··· 409 } 410 411 func (h *Handler) HandleCancelCommentEdit(w http.ResponseWriter, r *http.Request) { 412 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 413 if err != nil { 414 - log.Println("failed to get logged-in user:", err) 415 htmx.HxRedirect(w, "/login") 416 return 417 } ··· 419 rkey := chi.URLParam(r, "rkey") 420 comment, err := db.GetCommentByRkey(h.Db, user.Did, rkey) 421 if err != nil { 422 - log.Println("failed to get comment from db:", err) 423 htmx.HxError(w, http.StatusInternalServerError, "Failed to update comment, try again later.") 424 return 425 }
··· 1 package handlers 2 3 import ( 4 "net/http" 5 "strings" 6 "time" ··· 22 ) 23 24 func (h *Handler) HandleNewComment(w http.ResponseWriter, r *http.Request) { 25 + l := h.Logger.With("handler", "HandleNewComment") 26 + 27 client, err := h.Oauth.AuthorizedClient(r) 28 if err != nil { 29 + l.Error("failed to get authorized client", "err", err) 30 htmx.HxRedirect(w, "/login") 31 return 32 } 33 34 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 35 if err != nil { 36 + l.Error("failed to get logged-in user", "err", err) 37 htmx.HxRedirect(w, "/login") 38 return 39 } 40 41 profile, err := db.GetProfile(h.Db, user.Did) 42 if err != nil { 43 + l.Error("failed to get logged-in user", "err", err) 44 htmx.HxRedirect(w, "/login") 45 return 46 } 47 48 err = r.ParseForm() 49 if err != nil { 50 + l.Error("invalid comment form", "err", err) 51 htmx.HxError(w, http.StatusBadRequest, "Unable to process comment, please try again later.") 52 return 53 } 54 55 commentBody := r.FormValue("comment") 56 if len(strings.TrimSpace(commentBody)) == 0 { 57 + l.Error("invalid comment form: missing comment body") 58 htmx.HxError(w, http.StatusBadRequest, "Comment cannot be empty.") 59 return 60 } 61 62 studySessionUri := r.FormValue("study_session_uri") 63 if len(studySessionUri) == 0 { 64 + l.Error("invalid comment form: missing study session Uri") 65 htmx.HxError(w, http.StatusBadRequest, "Unable to create comment, please try again later.") 66 return 67 } ··· 101 }, 102 }) 103 if err != nil { 104 + l.Error("failed to create comment record", "err", err) 105 htmx.HxError(w, http.StatusInternalServerError, "Failed to create comment, try again later.") 106 return 107 } ··· 122 123 err = h.Posthog.Enqueue(event) 124 if err != nil { 125 + l.Error("failed to enqueue posthog event", "err", err) 126 } 127 } 128 ··· 154 } 155 156 func (h *Handler) HandleDeleteComment(w http.ResponseWriter, r *http.Request) { 157 + l := h.Logger.With("handler", "HandleDeleteComment") 158 + 159 user := h.Oauth.GetUser(r) 160 if user == nil { 161 + l.Error("failed to get logged-in user") 162 htmx.HxRedirect(w, "/login") 163 return 164 } 165 client, err := h.Oauth.AuthorizedClient(r) 166 if err != nil { 167 + l.Error("failed to get authorized client", "err", err) 168 htmx.HxRedirect(w, "/login") 169 return 170 } ··· 174 rkey := chi.URLParam(r, "rkey") 175 comment, err := db.GetCommentByRkey(h.Db, user.Did, rkey) 176 if err != nil { 177 + l.Error("failed to get comment from db", "err", err) 178 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete comment, try again later.") 179 return 180 } 181 182 if user.Did != comment.Did { 183 + l.Error("user does not own record", "did", user.Did, "commentDid", comment.Did) 184 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to delete this comment.") 185 return 186 } ··· 191 Rkey: comment.Rkey, 192 }) 193 if err != nil { 194 + l.Error("failed to delete comment from PDS", "err", err) 195 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete comment, try again later.") 196 return 197 } ··· 212 213 err = h.Posthog.Enqueue(event) 214 if err != nil { 215 + l.Error("failed to enqueue posthog event", "err", err) 216 } 217 } 218 ··· 221 } 222 223 func (h *Handler) HandleEditCommentPage(w http.ResponseWriter, r *http.Request) { 224 + l := h.Logger.With("handler", "HandleEditCommentPage") 225 + 226 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 227 if err != nil { 228 + l.Error("failed to get logged-in user", "err", err) 229 htmx.HxRedirect(w, "/login") 230 return 231 } ··· 233 rkey := chi.URLParam(r, "rkey") 234 comment, err := db.GetCommentByRkey(h.Db, user.Did, rkey) 235 if err != nil { 236 + l.Error("failed to get comment from db", "err", err) 237 htmx.HxError(w, http.StatusInternalServerError, "Failed to update comment, try again later.") 238 return 239 } 240 241 if user.Did != comment.Did { 242 + l.Error("user does not own record", "did", user.Did, "commentDid", comment.Did) 243 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this comment.") 244 return 245 } ··· 250 case http.MethodPost: 251 client, err := h.Oauth.AuthorizedClient(r) 252 if err != nil { 253 + l.Error("failed to get authorized client", "err", err) 254 htmx.HxRedirect(w, "/login") 255 return 256 } 257 258 err = r.ParseForm() 259 if err != nil { 260 + l.Error("invalid comment form", "err", err) 261 htmx.HxError(w, http.StatusBadRequest, "Unable to process comment, please try again later.") 262 return 263 } 264 265 commentBody := r.FormValue("comment") 266 if len(strings.TrimSpace(commentBody)) == 0 { 267 + l.Error("invalid comment form: missing comment body") 268 htmx.HxError(w, http.StatusBadRequest, "Comment cannot be empty.") 269 return 270 } ··· 308 SwapRecord: cid, 309 }) 310 if err != nil { 311 + l.Error("failed to update study session record", "err", err) 312 htmx.HxError(w, http.StatusInternalServerError, "Failed to update comment, try again later.") 313 return 314 } ··· 329 330 err = h.Posthog.Enqueue(event) 331 if err != nil { 332 + l.Error("failed to enqueue posthog event", "err", err) 333 } 334 } 335 ··· 392 } 393 394 func (h *Handler) HandleReply(w http.ResponseWriter, r *http.Request) { 395 + l := h.Logger.With("handler", "HandleReply") 396 + 397 user := h.Oauth.GetUser(r) 398 if user == nil { 399 + l.Error("failed to get logged-in user") 400 htmx.HxRedirect(w, "/login") 401 return 402 } ··· 404 studySessionUri := r.URL.Query().Get("root") 405 parentCommentUri := r.URL.Query().Get("parent") 406 if len(studySessionUri) == 0 || len(parentCommentUri) == 0 { 407 + l.Error("invalid reply form: study session uri or parent comment uri is empty") 408 htmx.HxError(w, http.StatusBadRequest, "Unable to process comment, please try again later.") 409 return 410 } ··· 416 } 417 418 func (h *Handler) HandleCancelCommentEdit(w http.ResponseWriter, r *http.Request) { 419 + l := h.Logger.With("handler", "HandleCancelCommentEdit") 420 + 421 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 422 if err != nil { 423 + l.Error("failed to get logged-in user", "err", err) 424 htmx.HxRedirect(w, "/login") 425 return 426 } ··· 428 rkey := chi.URLParam(r, "rkey") 429 comment, err := db.GetCommentByRkey(h.Db, user.Did, rkey) 430 if err != nil { 431 + l.Error("failed to get comment from db", "err", err) 432 htmx.HxError(w, http.StatusInternalServerError, "Failed to update comment, try again later.") 433 return 434 }
+11 -10
internal/server/handlers/follow.go
··· 1 package handlers 2 3 import ( 4 - "log" 5 "net/http" 6 "time" 7 ··· 20 ) 21 22 func (h *Handler) HandleFollow(w http.ResponseWriter, r *http.Request) { 23 client, err := h.Oauth.AuthorizedClient(r) 24 if err != nil { 25 - log.Println("failed to get authorized client:", err) 26 htmx.HxRedirect(w, "/login") 27 return 28 } 29 30 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 31 if err != nil { 32 - log.Println("failed to get logged-in user:", err) 33 htmx.HxRedirect(w, "/login") 34 return 35 } ··· 42 43 subjectIdent, err := h.IdResolver.ResolveIdent(r.Context(), subject) 44 if err != nil { 45 - log.Println("failed to follow, invalid did:", err) 46 htmx.HxError(w, http.StatusBadRequest, "Failed to follow profile, try again later.") 47 return 48 } 49 50 if user.Did == subjectIdent.DID.String() { 51 - log.Println("failed to follow, cannot follow yourself") 52 htmx.HxError(w, http.StatusBadRequest, "You cannot follow yourself.") 53 return 54 } ··· 68 }}, 69 }) 70 if err != nil { 71 - log.Println("failed to create follow record:", err) 72 htmx.HxError(w, http.StatusInternalServerError, "Failed to follow profile, try again later.") 73 return 74 } ··· 84 Set("is_mutual_follow", followStatus == db.IsMutual), 85 }) 86 if err != nil { 87 - log.Println("failed to enqueue posthog event:", err) 88 } 89 } 90 ··· 95 case http.MethodDelete: 96 follow, err := db.GetFollow(h.Db, user.Did, subjectIdent.DID.String()) 97 if err != nil { 98 - log.Println("failed to get follow relationship:", err) 99 htmx.HxError(w, http.StatusInternalServerError, "Failed to unfollow profile, try again later.") 100 return 101 } ··· 106 Rkey: follow.Rkey, 107 }) 108 if err != nil { 109 - log.Println("failed to delete follow record:", err) 110 htmx.HxError(w, http.StatusInternalServerError, "Failed to unfollow profile, try again later.") 111 return 112 } ··· 119 Set("subject_did", subjectIdent.DID.String()), 120 }) 121 if err != nil { 122 - log.Println("failed to enqueue posthog event:", err) 123 } 124 } 125
··· 1 package handlers 2 3 import ( 4 "net/http" 5 "time" 6 ··· 19 ) 20 21 func (h *Handler) HandleFollow(w http.ResponseWriter, r *http.Request) { 22 + l := h.Logger.With("handler", "HandleFollow") 23 + 24 client, err := h.Oauth.AuthorizedClient(r) 25 if err != nil { 26 + l.Error("failed to get authorized client", "err", err) 27 htmx.HxRedirect(w, "/login") 28 return 29 } 30 31 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 32 if err != nil { 33 + l.Error("failed to get logged-in user", "err", err) 34 htmx.HxRedirect(w, "/login") 35 return 36 } ··· 43 44 subjectIdent, err := h.IdResolver.ResolveIdent(r.Context(), subject) 45 if err != nil { 46 + l.Error("failed to follow, invalid did", "err", err) 47 htmx.HxError(w, http.StatusBadRequest, "Failed to follow profile, try again later.") 48 return 49 } 50 51 if user.Did == subjectIdent.DID.String() { 52 + l.Error("failed to follow, cannot follow yourself") 53 htmx.HxError(w, http.StatusBadRequest, "You cannot follow yourself.") 54 return 55 } ··· 69 }}, 70 }) 71 if err != nil { 72 + l.Error("failed to create follow record", "err", err) 73 htmx.HxError(w, http.StatusInternalServerError, "Failed to follow profile, try again later.") 74 return 75 } ··· 85 Set("is_mutual_follow", followStatus == db.IsMutual), 86 }) 87 if err != nil { 88 + l.Error("failed to enqueue posthog event", "err", err) 89 } 90 } 91 ··· 96 case http.MethodDelete: 97 follow, err := db.GetFollow(h.Db, user.Did, subjectIdent.DID.String()) 98 if err != nil { 99 + l.Error("failed to get follow relationship", "err", err) 100 htmx.HxError(w, http.StatusInternalServerError, "Failed to unfollow profile, try again later.") 101 return 102 } ··· 107 Rkey: follow.Rkey, 108 }) 109 if err != nil { 110 + l.Error("failed to delete follow record", "err", err) 111 htmx.HxError(w, http.StatusInternalServerError, "Failed to unfollow profile, try again later.") 112 return 113 } ··· 120 Set("subject_did", subjectIdent.DID.String()), 121 }) 122 if err != nil { 123 + l.Error("failed to enqueue posthog event", "err", err) 124 } 125 } 126
+29 -24
internal/server/handlers/activity.go
··· 3 import ( 4 "errors" 5 "fmt" 6 - "log" 7 "net/http" 8 "time" 9 ··· 55 } 56 57 func (h *Handler) HandleNewActivityPage(w http.ResponseWriter, r *http.Request) { 58 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 59 if err != nil { 60 - log.Println("failed to get logged-in user:", err) 61 htmx.HxRedirect(w, "/login") 62 return 63 } ··· 71 case http.MethodPost: 72 client, err := h.Oauth.AuthorizedClient(r) 73 if err != nil { 74 - log.Println("failed to get authorized client:", err) 75 htmx.HxRedirect(w, "/login") 76 return 77 } 78 79 newActivity, err := parseActivityForm(r) 80 if err != nil { 81 - log.Println("invalid activity form:", err) 82 htmx.HxError(w, http.StatusBadRequest, "Failed to create activity, ensure all fields contain valid data.") 83 return 84 } ··· 87 newActivity.CreatedAt = time.Now().UTC() 88 89 if err := db.ValidateActivity(newActivity); err != nil { 90 - log.Println("invalid activity def:", err) 91 switch { 92 case errors.Is(err, db.ErrActivityNameEmpty): 93 htmx.HxError(w, http.StatusBadRequest, "Activity must have a name.") ··· 123 }, 124 }) 125 if err != nil { 126 - log.Println("failed to create activity record:", err) 127 htmx.HxError(w, http.StatusInternalServerError, "Failed to create activity, try again later.") 128 return 129 } 130 131 err = SavePendingCreate(h, w, r, PendingActivityCreation, newActivity) 132 if err != nil { 133 - log.Printf("failed to save yoten-session to add pending activity creation: %v", err) 134 } 135 136 if !h.Config.Core.Dev { ··· 145 Set("category_count", len(categoriesString)), 146 }) 147 if err != nil { 148 - log.Println("failed to enqueue posthog event:", err) 149 } 150 } 151 ··· 154 } 155 156 func (h *Handler) HandleDeleteActivity(w http.ResponseWriter, r *http.Request) { 157 user := h.Oauth.GetUser(r) 158 if user == nil { 159 - log.Println("failed to get logged-in user") 160 htmx.HxRedirect(w, "/login") 161 return 162 } 163 client, err := h.Oauth.AuthorizedClient(r) 164 if err != nil { 165 - log.Println("failed to get authorized client:", err) 166 htmx.HxError(w, http.StatusUnauthorized, "Failed to delete activity, try again later.") 167 return 168 } ··· 172 rkey := chi.URLParam(r, "rkey") 173 activity, err := db.GetActivityByRkey(h.Db, user.Did, rkey) 174 if err != nil { 175 - log.Println("failed to get activity from db:", err) 176 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete activity, try again later.") 177 return 178 } 179 180 if user.Did != activity.Did { 181 - log.Printf("user '%s' does not own record '%s'", user.Did, rkey) 182 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this activity.") 183 return 184 } ··· 189 Rkey: activity.Rkey, 190 }) 191 if err != nil { 192 - log.Println("failed to delete activity from PDS:", err) 193 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete activity, try again later.") 194 return 195 } 196 197 err = SavePendingDelete(h, w, r, PendingActivityDeletion, activity) 198 if err != nil { 199 - log.Printf("failed to save yoten-session to add pending activity deletion: %v", err) 200 } 201 202 if !h.Config.Core.Dev { ··· 207 Set("activity_id", activity.ID), 208 }) 209 if err != nil { 210 - log.Println("failed to enqueue posthog event:", err) 211 } 212 } 213 ··· 216 } 217 218 func (h *Handler) HandleEditActivityPage(w http.ResponseWriter, r *http.Request) { 219 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 220 if err != nil { 221 - log.Println("failed to get logged-in user:", err) 222 htmx.HxRedirect(w, "/login") 223 return 224 } ··· 226 rkey := chi.URLParam(r, "rkey") 227 activity, err := db.GetActivityByRkey(h.Db, user.Did, rkey) 228 if err != nil { 229 - log.Println("failed to get activity from db:", err) 230 htmx.HxError(w, http.StatusInternalServerError, "Failed to update activity, try again later.") 231 return 232 } 233 234 if user.Did != activity.Did { 235 - log.Printf("user '%s' does not own record '%s'", user.Did, rkey) 236 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this activity.") 237 return 238 } ··· 247 case http.MethodPost: 248 client, err := h.Oauth.AuthorizedClient(r) 249 if err != nil { 250 - log.Println("failed to get authorized client:", err) 251 htmx.HxRedirect(w, "/login") 252 return 253 } 254 255 updatedActivity, err := parseActivityForm(r) 256 if err != nil { 257 - log.Println("invalid activity form:", err) 258 htmx.HxError(w, http.StatusBadRequest, "Failed to create activity, ensure all fields contain valid data.") 259 return 260 } ··· 263 updatedActivity.CreatedAt = activity.CreatedAt 264 265 if err := db.ValidateActivity(updatedActivity); err != nil { 266 - log.Println("invalid activity def:", err) 267 switch { 268 case errors.Is(err, db.ErrActivityNameEmpty): 269 htmx.HxError(w, http.StatusBadRequest, "Activity must have a name.") ··· 306 SwapRecord: cid, 307 }) 308 if err != nil { 309 - log.Println("failed to update study session record:", err) 310 htmx.HxError(w, http.StatusInternalServerError, "Failed to update activity, try again later.") 311 return 312 } 313 314 err = SavePendingUpdate(h, w, r, PendingActivityUpdates, updatedActivity) 315 if err != nil { 316 - log.Printf("failed to save yoten-session to add pending activity updates: %v", err) 317 } 318 319 if !h.Config.Core.Dev { ··· 328 Set("category_count", len(categoriesString)), 329 }) 330 if err != nil { 331 - log.Println("failed to enqueue posthog event:", err) 332 } 333 } 334
··· 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "time" 8 ··· 54 } 55 56 func (h *Handler) HandleNewActivityPage(w http.ResponseWriter, r *http.Request) { 57 + l := h.Logger.With("handler", "HandleNewActivityPage") 58 + 59 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 60 if err != nil { 61 + l.Error("failed to get logged-in user", "err", err) 62 htmx.HxRedirect(w, "/login") 63 return 64 } ··· 72 case http.MethodPost: 73 client, err := h.Oauth.AuthorizedClient(r) 74 if err != nil { 75 + l.Error("failed to get authorized client", "err", err) 76 htmx.HxRedirect(w, "/login") 77 return 78 } 79 80 newActivity, err := parseActivityForm(r) 81 if err != nil { 82 + l.Error("invalid activity form", "err", err) 83 htmx.HxError(w, http.StatusBadRequest, "Failed to create activity, ensure all fields contain valid data.") 84 return 85 } ··· 88 newActivity.CreatedAt = time.Now().UTC() 89 90 if err := db.ValidateActivity(newActivity); err != nil { 91 + l.Error("invalid activity def", "err", err) 92 switch { 93 case errors.Is(err, db.ErrActivityNameEmpty): 94 htmx.HxError(w, http.StatusBadRequest, "Activity must have a name.") ··· 124 }, 125 }) 126 if err != nil { 127 + l.Error("failed to create activity record", "err", err) 128 htmx.HxError(w, http.StatusInternalServerError, "Failed to create activity, try again later.") 129 return 130 } 131 132 err = SavePendingCreate(h, w, r, PendingActivityCreation, newActivity) 133 if err != nil { 134 + l.Error("failed to save yoten-session to add pending activity creation", "err", err) 135 } 136 137 if !h.Config.Core.Dev { ··· 146 Set("category_count", len(categoriesString)), 147 }) 148 if err != nil { 149 + l.Error("failed to enqueue posthog event", "err", err) 150 } 151 } 152 ··· 155 } 156 157 func (h *Handler) HandleDeleteActivity(w http.ResponseWriter, r *http.Request) { 158 + l := h.Logger.With("handler", "HandleDeleteActivity") 159 + 160 user := h.Oauth.GetUser(r) 161 if user == nil { 162 + l.Error("failed to get logged-in user") 163 htmx.HxRedirect(w, "/login") 164 return 165 } 166 client, err := h.Oauth.AuthorizedClient(r) 167 if err != nil { 168 + l.Error("failed to get authorized client", "err", err) 169 htmx.HxError(w, http.StatusUnauthorized, "Failed to delete activity, try again later.") 170 return 171 } ··· 175 rkey := chi.URLParam(r, "rkey") 176 activity, err := db.GetActivityByRkey(h.Db, user.Did, rkey) 177 if err != nil { 178 + l.Error("failed to get activity from db", "err", err) 179 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete activity, try again later.") 180 return 181 } 182 183 if user.Did != activity.Did { 184 + l.Error("user does not own record", "did", user.Did, "activityDid", activity.Did) 185 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this activity.") 186 return 187 } ··· 192 Rkey: activity.Rkey, 193 }) 194 if err != nil { 195 + l.Error("failed to delete activity from PDS", "err", err) 196 htmx.HxError(w, http.StatusInternalServerError, "Failed to delete activity, try again later.") 197 return 198 } 199 200 err = SavePendingDelete(h, w, r, PendingActivityDeletion, activity) 201 if err != nil { 202 + l.Error("failed to save yoten-session to add pending activity deletion", "err", err) 203 } 204 205 if !h.Config.Core.Dev { ··· 210 Set("activity_id", activity.ID), 211 }) 212 if err != nil { 213 + l.Error("failed to enqueue posthog event", "err", err) 214 } 215 } 216 ··· 219 } 220 221 func (h *Handler) HandleEditActivityPage(w http.ResponseWriter, r *http.Request) { 222 + l := h.Logger.With("handler", "HandleEditActivityPage") 223 + 224 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 225 if err != nil { 226 + l.Error("failed to get logged-in user", "err", err) 227 htmx.HxRedirect(w, "/login") 228 return 229 } ··· 231 rkey := chi.URLParam(r, "rkey") 232 activity, err := db.GetActivityByRkey(h.Db, user.Did, rkey) 233 if err != nil { 234 + l.Error("failed to get activity from db", "err", err) 235 htmx.HxError(w, http.StatusInternalServerError, "Failed to update activity, try again later.") 236 return 237 } 238 239 if user.Did != activity.Did { 240 + l.Error("user does not own record", "did", user.Did, "activityDid", activity.Did) 241 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this activity.") 242 return 243 } ··· 252 case http.MethodPost: 253 client, err := h.Oauth.AuthorizedClient(r) 254 if err != nil { 255 + l.Error("failed to get authorized client", "err", err) 256 htmx.HxRedirect(w, "/login") 257 return 258 } 259 260 updatedActivity, err := parseActivityForm(r) 261 if err != nil { 262 + l.Error("invalid activity form", "err", err) 263 htmx.HxError(w, http.StatusBadRequest, "Failed to create activity, ensure all fields contain valid data.") 264 return 265 } ··· 268 updatedActivity.CreatedAt = activity.CreatedAt 269 270 if err := db.ValidateActivity(updatedActivity); err != nil { 271 + l.Error("invalid activity def", "err", err) 272 switch { 273 case errors.Is(err, db.ErrActivityNameEmpty): 274 htmx.HxError(w, http.StatusBadRequest, "Activity must have a name.") ··· 311 SwapRecord: cid, 312 }) 313 if err != nil { 314 + l.Error("failed to update study session record", "err", err) 315 htmx.HxError(w, http.StatusInternalServerError, "Failed to update activity, try again later.") 316 return 317 } 318 319 err = SavePendingUpdate(h, w, r, PendingActivityUpdates, updatedActivity) 320 if err != nil { 321 + l.Error("failed to save yoten-session to add pending activity updates", "err", err) 322 } 323 324 if !h.Config.Core.Dev { ··· 333 Set("category_count", len(categoriesString)), 334 }) 335 if err != nil { 336 + l.Error("failed to enqueue posthog event", "err", err) 337 } 338 } 339
+26 -18
internal/server/handlers/login.go
··· 3 import ( 4 "context" 5 "fmt" 6 - "log" 7 "net/http" 8 "strings" 9 ··· 17 ) 18 19 func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { 20 switch r.Method { 21 case http.MethodGet: 22 var user *types.User ··· 24 if oauth != nil { 25 bskyProfile, err := bsky.GetBskyProfile(oauth.Did) 26 if err != nil { 27 - log.Println("failed to get bsky profile:", err) 28 } 29 user = &types.User{ 30 OauthUser: *oauth, ··· 54 55 // Basic handle validation 56 if !strings.Contains(handle, ".") { 57 - log.Println("invalid handle format:", handle) 58 - htmx.HxError(w, http.StatusBadGateway, fmt.Sprintf("'%s' is an invalid handle. Did you mean %s.bsky.social?", handle, handle)) 59 return 60 } 61 62 - 63 - 64 - 65 - 66 - 67 Event: ph.UserSignInInitiatedEvent, 68 }) 69 if err != nil { 70 - log.Println("failed to enqueue posthog event:", err) 71 } 72 } 73 } 74 75 76 77 78 - 79 - 80 - 81 - 82 - 83 } 84 85 func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { 86 did := h.Oauth.GetDid(r) 87 88 err := h.Oauth.DeleteSession(w, r) 89 if err != nil { 90 - log.Println("failed to logout", "err", err) 91 } else { 92 - log.Println("logged out successfully") 93 } 94 95 if !h.Config.Core.Dev && did != "" { ··· 98 Event: ph.UserLoggedOutEvent, 99 }) 100 if err != nil { 101 - log.Println("failed to enqueue posthog event:", err) 102 } 103 } 104
··· 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "strings" 8 ··· 16 ) 17 18 func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { 19 + l := h.Logger.With("handler", "Login") 20 + 21 switch r.Method { 22 case http.MethodGet: 23 var user *types.User ··· 25 if oauth != nil { 26 bskyProfile, err := bsky.GetBskyProfile(oauth.Did) 27 if err != nil { 28 + l.Error("failed to get bsky profile", "err", err) 29 } 30 user = &types.User{ 31 OauthUser: *oauth, ··· 55 56 // Basic handle validation 57 if !strings.Contains(handle, ".") { 58 + l.Error("invalid handle format", "handle", handle) 59 + htmx.HxError(w, http.StatusBadRequest, fmt.Sprintf("'%s' is an invalid handle. Did you mean %s.bsky.social?", handle, handle)) 60 return 61 } 62 63 + resolved, err := h.IdResolver.ResolveIdent(context.Background(), handle) 64 + if err != nil { 65 + l.Error("failed to resolve handle", "handle", handle, "err", err) 66 + htmx.HxError(w, http.StatusBadRequest, fmt.Sprintf("'%s' is an invalid handle", handle)) 67 + return 68 + } else { 69 + if !h.Config.Core.Dev && resolved.DID.String() != "" { 70 + err := h.Posthog.Enqueue(posthog.Capture{ 71 + DistinctId: string(resolved.DID), 72 Event: ph.UserSignInInitiatedEvent, 73 }) 74 if err != nil { 75 + l.Error("failed to enqueue posthog event", "err", err) 76 } 77 } 78 } 79 80 + redirectURL, err := h.Oauth.ClientApp.StartAuthFlow(r.Context(), handle) 81 + if err != nil { 82 + l.Error("failed to resolve auth flow", "handle", handle, "err", err) 83 + http.Error(w, err.Error(), http.StatusInternalServerError) 84 + return 85 + } 86 87 88 89 } 90 91 func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { 92 + l := h.Logger.With("handler", "Logout") 93 + 94 did := h.Oauth.GetDid(r) 95 96 err := h.Oauth.DeleteSession(w, r) 97 if err != nil { 98 + l.Error("failed to logout", "err", err) 99 } else { 100 + l.Error("logged out successfully") 101 } 102 103 if !h.Config.Core.Dev && did != "" { ··· 106 Event: ph.UserLoggedOutEvent, 107 }) 108 if err != nil { 109 + l.Error("failed to enqueue posthog event", "err", err) 110 } 111 } 112
+10 -7
internal/server/handlers/notification.go
··· 1 package handlers 2 3 import ( 4 - "log" 5 "net/http" 6 "strconv" 7 ··· 12 ) 13 14 func (h *Handler) HandleNotificationFeed(w http.ResponseWriter, r *http.Request) { 15 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 16 if err != nil { 17 - log.Println("failed to get logged-in user:", err) 18 htmx.HxRedirect(w, "/login") 19 return 20 } ··· 25 } 26 page, err := strconv.ParseInt(pageStr, 10, 64) 27 if err != nil { 28 - log.Println("failed to parse page value:", err) 29 page = 1 30 } 31 if page == 0 { ··· 39 case http.MethodGet: 40 notifications, err := db.GetNotificationsByDid(h.Db, user.Did, pageSize+1, int(offset)) 41 if err != nil { 42 - log.Println("failed to retrieve notifications:", err) 43 htmx.HxError(w, http.StatusInternalServerError, "Failed to get notifications, try again later.") 44 return 45 } 46 47 hydratedNotifications, err := h.getBskyProfileHydratedNotificationFeed(notifications) 48 if err != nil { 49 - log.Println("failed to hydrate notifications with bsky profile:", err) 50 htmx.HxError(w, http.StatusInternalServerError, "Failed to get notifications, try again later.") 51 return 52 } ··· 66 } 67 68 func (h *Handler) HandleNotificationMarkAllRead(w http.ResponseWriter, r *http.Request) { 69 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 70 if err != nil { 71 - log.Println("failed to get logged-in user:", err) 72 htmx.HxRedirect(w, "/login") 73 return 74 } 75 76 err = db.MarkAllNotificationsAsRead(h.Db, user.Did) 77 if err != nil { 78 - log.Println("failed to mark all notifications:", err) 79 htmx.HxError(w, http.StatusInternalServerError, "Failed to mark all notifications as read, try again later.") 80 return 81 }
··· 1 package handlers 2 3 import ( 4 "net/http" 5 "strconv" 6 ··· 11 ) 12 13 func (h *Handler) HandleNotificationFeed(w http.ResponseWriter, r *http.Request) { 14 + l := h.Logger.With("handler", "HandleNotificationFeed") 15 + 16 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 17 if err != nil { 18 + l.Error("failed to get logged-in user", "err", err) 19 htmx.HxRedirect(w, "/login") 20 return 21 } ··· 26 } 27 page, err := strconv.ParseInt(pageStr, 10, 64) 28 if err != nil { 29 + l.Error("failed to parse page value", "err", err) 30 page = 1 31 } 32 if page == 0 { ··· 40 case http.MethodGet: 41 notifications, err := db.GetNotificationsByDid(h.Db, user.Did, pageSize+1, int(offset)) 42 if err != nil { 43 + l.Error("failed to retrieve notifications", "err", err) 44 htmx.HxError(w, http.StatusInternalServerError, "Failed to get notifications, try again later.") 45 return 46 } 47 48 hydratedNotifications, err := h.getBskyProfileHydratedNotificationFeed(notifications) 49 if err != nil { 50 + l.Error("failed to hydrate notifications with bsky profile", "err", err) 51 htmx.HxError(w, http.StatusInternalServerError, "Failed to get notifications, try again later.") 52 return 53 } ··· 67 } 68 69 func (h *Handler) HandleNotificationMarkAllRead(w http.ResponseWriter, r *http.Request) { 70 + l := h.Logger.With("handler", "HandleNotificationMarkAllRead") 71 + 72 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 73 if err != nil { 74 + l.Error("failed to get logged-in user", "err", err) 75 htmx.HxRedirect(w, "/login") 76 return 77 } 78 79 err = db.MarkAllNotificationsAsRead(h.Db, user.Did) 80 if err != nil { 81 + l.Error("failed to mark all notifications", "err", err) 82 htmx.HxError(w, http.StatusInternalServerError, "Failed to mark all notifications as read, try again later.") 83 return 84 }
+56 -40
internal/server/handlers/profile.go
··· 3 import ( 4 "errors" 5 "fmt" 6 - "log" 7 "net/http" 8 "strconv" 9 "time" ··· 30 PendingProfileUpdate string = "pending_profile_update" 31 ) 32 33 - func parseProfileForm(r *http.Request) (db.Profile, error) { 34 err := r.ParseForm() 35 if err != nil { 36 return db.Profile{}, fmt.Errorf("invalid profile form: %w", err) ··· 45 for _, code := range languageCodes { 46 language, ok := db.Languages[db.LanguageCode(code)] 47 if !ok { 48 - log.Printf("invalid language code submitted: %s", code) 49 continue 50 } 51 languages = append(languages, language) ··· 62 } 63 64 func (h *Handler) HandleProfilePage(w http.ResponseWriter, r *http.Request) { 65 didOrHandle := chi.URLParam(r, "user") 66 if didOrHandle == "" { 67 http.Error(w, "Bad request", http.StatusBadRequest) ··· 102 103 totalStudyTime, err = db.GetTotalStudyTime(h.Db, profileDid) 104 if err != nil { 105 - log.Println("failed to get total study time:", err) 106 } 107 108 totalStudySessions, _ = db.GetTotalStudySessions(h.Db, profileDid) 109 if err != nil { 110 - log.Println("failed to get total study study sessions:", err) 111 } 112 113 followers, following, _ = db.GetFollowerFollowingCount(h.Db, profileDid) 114 if err != nil { 115 - log.Println("failed to get follow stats:", err) 116 } 117 118 streak, _ = db.GetCurrentStreak(h.Db, profileDid) 119 if err != nil { 120 - log.Println("failed to get streak:", err) 121 } 122 123 if user != nil { ··· 128 }) 129 130 if err := g.Wait(); err != nil { 131 - log.Printf("failed to fetch critical profile data for %s: %v", profileDid, err) 132 htmx.HxError(w, http.StatusInternalServerError, "Failed to fetch profile data, try again later.") 133 return 134 } ··· 160 161 err := h.Posthog.Enqueue(capture) 162 if err != nil { 163 - log.Println("failed to enqueue posthog event:", err) 164 } 165 } 166 ··· 178 } 179 180 func (h *Handler) HandleEditProfilePage(w http.ResponseWriter, r *http.Request) { 181 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 182 if err != nil { 183 - log.Println("failed to get logged-in user:", err) 184 htmx.HxRedirect(w, "/login") 185 return 186 } 187 188 profile, err := h.GetUserProfileWithAvatar(user.Did) 189 if err != nil { 190 - log.Printf("failed to find %s in db: %s", user.Did, err) 191 w.WriteHeader(http.StatusNotFound) 192 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 193 return 194 } 195 196 if user.Did != profile.Did { 197 - log.Printf("user '%s' does not own record '%s'", user.Did, profile.Did) 198 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this profile.") 199 return 200 } ··· 214 case http.MethodPost: 215 client, err := h.Oauth.AuthorizedClient(r) 216 if err != nil { 217 - log.Println("failed to get authorized client:", err) 218 htmx.HxRedirect(w, "/login") 219 return 220 } 221 222 - updatedProfile, err := parseProfileForm(r) 223 if err != nil { 224 - log.Println("invalid profile form:", err) 225 htmx.HxError(w, http.StatusBadRequest, "Failed to update profile, ensure all fields contain valid data.") 226 return 227 } ··· 236 } 237 238 if err := db.ValidateProfile(updatedProfile); err != nil { 239 - log.Println("invalid profile:", err) 240 switch { 241 case errors.Is(err, db.ErrProfileNameTooLong): 242 htmx.HxError(w, http.StatusBadRequest, "Profile name cannot be more than 64 characters.") ··· 280 SwapRecord: cid, 281 }) 282 if err != nil { 283 - log.Println("failed to put profile record:", err) 284 htmx.HxError(w, http.StatusInternalServerError, "Failed to update PDS, try again later.") 285 return 286 } 287 288 err = SavePendingUpdate(h, w, r, PendingProfileUpdate, profile) 289 if err != nil { 290 - log.Printf("failed to save yoten-session to add pending profile update: %v", err) 291 } 292 293 if !h.Config.Core.Dev { ··· 308 Properties: properties, 309 }) 310 if err != nil { 311 - log.Println("failed to enqueue posthog identify event:", err) 312 } 313 314 err = h.Posthog.Enqueue(posthog.Capture{ ··· 316 Event: ph.ProfileRecordEditedEvent, 317 }) 318 if err != nil { 319 - log.Println("failed to enqueue posthog event:", err) 320 } 321 } 322 ··· 325 } 326 327 func (h *Handler) HandleResourcesPage(w http.ResponseWriter, r *http.Request) { 328 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 329 if err != nil { 330 - log.Println("failed to get logged-in user:", err) 331 htmx.HxRedirect(w, "/login") 332 return 333 } 334 335 resources, err := db.GetResourcesByDid(h.Db, user.Did) 336 if err != nil { 337 - log.Println("failed to get resources:", err) 338 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve profile resources, try again later.") 339 return 340 } 341 342 resources, err = ApplyPendingChanges(h, w, r, resources, PendingResourceCreation, PendingResourceUpdates, PendingResourceDeletion) 343 if err != nil { 344 - log.Printf("failed to save yoten-session after processing pending changes: %v", err) 345 } 346 347 activeResources := utils.Filter(resources, func(resource db.Resource) bool { ··· 355 } 356 357 func (h *Handler) HandleActivitiesPage(w http.ResponseWriter, r *http.Request) { 358 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 359 if err != nil { 360 - log.Println("failed to get logged-in user:", err) 361 htmx.HxRedirect(w, "/login") 362 return 363 } 364 365 activities, err := db.GetActivitiesByDid(h.Db, user.Did) 366 if err != nil { 367 - log.Println("failed to get activities:", err) 368 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve profile activities, try again later.") 369 return 370 } 371 372 activities, err = ApplyPendingChanges(h, w, r, activities, PendingActivityCreation, PendingActivityUpdates, PendingActivityDeletion) 373 if err != nil { 374 - log.Printf("failed to save yoten-session after processing pending changes: %v", err) 375 } 376 377 activeActivities := utils.Filter(activities, func(activity db.Activity) bool { ··· 400 } 401 402 func (h *Handler) HandleProfileFeed(w http.ResponseWriter, r *http.Request) { 403 didOrHandle := chi.URLParam(r, "user") 404 if didOrHandle == "" { 405 http.Error(w, "Bad request", http.StatusBadRequest) ··· 415 416 profile, err := h.GetUserProfileWithAvatar(ident.DID.String()) 417 if err != nil { 418 - log.Printf("failed to find %s in db: %s", ident.DID.String(), err) 419 w.WriteHeader(http.StatusNotFound) 420 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 421 return ··· 423 424 bskyProfile, err := bsky.GetBskyProfile(ident.DID.String()) 425 if err != nil { 426 - log.Println("failed to get bsky profile:", err) 427 w.WriteHeader(http.StatusNotFound) 428 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 429 return ··· 435 } 436 page, err := strconv.ParseInt(pageStr, 10, 64) 437 if err != nil { 438 - log.Println("failed to parse page value:", err) 439 page = 1 440 } 441 if page == 0 { ··· 447 448 sessions, err := db.GetStudySessionLogs(h.Db, ident.DID.String(), pageSize+1, int(offset)) 449 if err != nil { 450 - log.Println("failed to get study sessions:", err) 451 w.WriteHeader(http.StatusNotFound) 452 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 453 return ··· 455 456 sessions, err = ApplyPendingChanges(h, w, r, sessions, PendingStudySessionCreation, PendingStudySessionUpdates, PendingStudySessionDeletion) 457 if err != nil { 458 - log.Printf("failed to save yoten-session after processing pending changes: %v", err) 459 } 460 461 feed := []*db.StudySessionFeedItem{} ··· 493 } 494 495 func (h *Handler) HandleFriendsPage(w http.ResponseWriter, r *http.Request) { 496 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 497 if err != nil { 498 - log.Println("failed to get logged-in user:", err) 499 htmx.HxRedirect(w, "/login") 500 return 501 } 502 503 followers, following, err := db.GetFollowerFollowingCount(h.Db, user.Did) 504 if err != nil { 505 - log.Printf("getting follow stats repos for %s: %s", user.Did, err) 506 } 507 508 views.FriendsPage(views.FriendsPageParams{ ··· 513 } 514 515 func (h *Handler) HandleFriendsFeed(w http.ResponseWriter, r *http.Request) { 516 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 517 if err != nil { 518 - log.Println("failed to get logged-in user") 519 htmx.HxRedirect(w, "/login") 520 return 521 } ··· 529 } 530 page, err := strconv.ParseInt(pageStr, 10, 64) 531 if err != nil { 532 - log.Println("failed to parse page value:", err) 533 page = 1 534 } 535 if page == 0 { ··· 545 if mode == partials.Following { 546 feed, err := db.GetFollowing(h.Db, user.Did, pageSize+1, int(offset)) 547 if err != nil { 548 - log.Println("failed to get following list:", err) 549 htmx.HxError(w, http.StatusInternalServerError, "Failed to get following list, try again later.") 550 return 551 } 552 bskyHydratedFeed, err = h.GetHydratedFollowerProfiles(feed) 553 if err != nil { 554 - log.Println("failed to hydrate bsky profiles:", err) 555 htmx.HxError(w, http.StatusInternalServerError, "Failed to get following list, try again later.") 556 return 557 } 558 } else { 559 feed, err := db.GetFollowers(h.Db, user.Did, pageSize+1, int(offset)) 560 if err != nil { 561 - log.Println("failed to get followers list:", err) 562 htmx.HxError(w, http.StatusInternalServerError, "Failed to get followers list, try again later.") 563 return 564 } 565 bskyHydratedFeed, err = h.GetHydratedFollowerProfiles(feed) 566 if err != nil { 567 - log.Println("failed to hydrate bsky profiles:", err) 568 htmx.HxError(w, http.StatusInternalServerError, "Failed to get following list, try again later.") 569 return 570 } ··· 586 } 587 588 func (h *Handler) HandleNotificationsPage(w http.ResponseWriter, r *http.Request) { 589 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 590 if err != nil { 591 - log.Println("failed to get logged-in user:", err) 592 htmx.HxRedirect(w, "/login") 593 return 594 }
··· 3 import ( 4 "errors" 5 "fmt" 6 + "log/slog" 7 "net/http" 8 "strconv" 9 "time" ··· 30 PendingProfileUpdate string = "pending_profile_update" 31 ) 32 33 + func parseProfileForm(r *http.Request, logger *slog.Logger) (db.Profile, error) { 34 err := r.ParseForm() 35 if err != nil { 36 return db.Profile{}, fmt.Errorf("invalid profile form: %w", err) ··· 45 for _, code := range languageCodes { 46 language, ok := db.Languages[db.LanguageCode(code)] 47 if !ok { 48 + logger.Warn("invalid language code submitted", "languageCode", code) 49 continue 50 } 51 languages = append(languages, language) ··· 62 } 63 64 func (h *Handler) HandleProfilePage(w http.ResponseWriter, r *http.Request) { 65 + l := h.Logger.With("handler", "HandleProfilePage") 66 + 67 didOrHandle := chi.URLParam(r, "user") 68 if didOrHandle == "" { 69 http.Error(w, "Bad request", http.StatusBadRequest) ··· 104 105 totalStudyTime, err = db.GetTotalStudyTime(h.Db, profileDid) 106 if err != nil { 107 + l.Error("failed to get total study time", "err", err) 108 } 109 110 totalStudySessions, _ = db.GetTotalStudySessions(h.Db, profileDid) 111 if err != nil { 112 + l.Error("failed to get total study study sessions", "err", err) 113 } 114 115 followers, following, _ = db.GetFollowerFollowingCount(h.Db, profileDid) 116 if err != nil { 117 + l.Error("failed to get follow stats", "err", err) 118 } 119 120 streak, _ = db.GetCurrentStreak(h.Db, profileDid) 121 if err != nil { 122 + l.Error("failed to get streak", "err", err) 123 } 124 125 if user != nil { ··· 130 }) 131 132 if err := g.Wait(); err != nil { 133 + l.Error("failed to fetch critical profile data", "did", profileDid, "err", err) 134 htmx.HxError(w, http.StatusInternalServerError, "Failed to fetch profile data, try again later.") 135 return 136 } ··· 162 163 err := h.Posthog.Enqueue(capture) 164 if err != nil { 165 + l.Error("failed to enqueue posthog event", "err", err) 166 } 167 } 168 ··· 180 } 181 182 func (h *Handler) HandleEditProfilePage(w http.ResponseWriter, r *http.Request) { 183 + l := h.Logger.With("handler", "HandleEditProfilePage") 184 + 185 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 186 if err != nil { 187 + l.Error("failed to get logged-in user", "err", err) 188 htmx.HxRedirect(w, "/login") 189 return 190 } 191 192 profile, err := h.GetUserProfileWithAvatar(user.Did) 193 if err != nil { 194 + l.Error("failed to find user in db", "did", user.Did, "err", err) 195 w.WriteHeader(http.StatusNotFound) 196 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 197 return 198 } 199 200 if user.Did != profile.Did { 201 + l.Error("user does not own record", "did", user.Did, "profileDid", profile.Did) 202 htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this profile.") 203 return 204 } ··· 218 case http.MethodPost: 219 client, err := h.Oauth.AuthorizedClient(r) 220 if err != nil { 221 + l.Error("failed to get authorized client", "err", err) 222 htmx.HxRedirect(w, "/login") 223 return 224 } 225 226 + updatedProfile, err := parseProfileForm(r, l) 227 if err != nil { 228 + l.Error("invalid profile form", "err", err) 229 htmx.HxError(w, http.StatusBadRequest, "Failed to update profile, ensure all fields contain valid data.") 230 return 231 } ··· 240 } 241 242 if err := db.ValidateProfile(updatedProfile); err != nil { 243 + l.Error("invalid profile", "err", err) 244 switch { 245 case errors.Is(err, db.ErrProfileNameTooLong): 246 htmx.HxError(w, http.StatusBadRequest, "Profile name cannot be more than 64 characters.") ··· 284 SwapRecord: cid, 285 }) 286 if err != nil { 287 + l.Error("failed to put profile record", "err", err) 288 htmx.HxError(w, http.StatusInternalServerError, "Failed to update PDS, try again later.") 289 return 290 } 291 292 err = SavePendingUpdate(h, w, r, PendingProfileUpdate, profile) 293 if err != nil { 294 + l.Error("failed to save yoten-session to add pending profile update", "err", err) 295 } 296 297 if !h.Config.Core.Dev { ··· 312 Properties: properties, 313 }) 314 if err != nil { 315 + l.Error("failed to enqueue posthog identify event", "err", err) 316 } 317 318 err = h.Posthog.Enqueue(posthog.Capture{ ··· 320 Event: ph.ProfileRecordEditedEvent, 321 }) 322 if err != nil { 323 + l.Error("failed to enqueue posthog event", "err", err) 324 } 325 } 326 ··· 329 } 330 331 func (h *Handler) HandleResourcesPage(w http.ResponseWriter, r *http.Request) { 332 + l := h.Logger.With("handler", "HandleResourcesPage") 333 + 334 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 335 if err != nil { 336 + l.Error("failed to get logged-in user", "err", err) 337 htmx.HxRedirect(w, "/login") 338 return 339 } 340 341 resources, err := db.GetResourcesByDid(h.Db, user.Did) 342 if err != nil { 343 + l.Error("failed to get resources", "err", err) 344 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve profile resources, try again later.") 345 return 346 } 347 348 resources, err = ApplyPendingChanges(h, w, r, resources, PendingResourceCreation, PendingResourceUpdates, PendingResourceDeletion) 349 if err != nil { 350 + l.Error("failed to save yoten-session after processing pending changes", "err", err) 351 } 352 353 activeResources := utils.Filter(resources, func(resource db.Resource) bool { ··· 361 } 362 363 func (h *Handler) HandleActivitiesPage(w http.ResponseWriter, r *http.Request) { 364 + l := h.Logger.With("handler", "HandleActivitiesPage") 365 + 366 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 367 if err != nil { 368 + l.Error("failed to get logged-in user", "err", err) 369 htmx.HxRedirect(w, "/login") 370 return 371 } 372 373 activities, err := db.GetActivitiesByDid(h.Db, user.Did) 374 if err != nil { 375 + l.Error("failed to get activities", "err", err) 376 htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve profile activities, try again later.") 377 return 378 } 379 380 activities, err = ApplyPendingChanges(h, w, r, activities, PendingActivityCreation, PendingActivityUpdates, PendingActivityDeletion) 381 if err != nil { 382 + l.Error("failed to save yoten-session after processing pending changes", "err", err) 383 } 384 385 activeActivities := utils.Filter(activities, func(activity db.Activity) bool { ··· 408 } 409 410 func (h *Handler) HandleProfileFeed(w http.ResponseWriter, r *http.Request) { 411 + l := h.Logger.With("handler", "HandleProfileFeed") 412 + 413 didOrHandle := chi.URLParam(r, "user") 414 if didOrHandle == "" { 415 http.Error(w, "Bad request", http.StatusBadRequest) ··· 425 426 profile, err := h.GetUserProfileWithAvatar(ident.DID.String()) 427 if err != nil { 428 + l.Error("failed to find user in db", "did", ident.DID.String(), "err", err) 429 w.WriteHeader(http.StatusNotFound) 430 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 431 return ··· 433 434 bskyProfile, err := bsky.GetBskyProfile(ident.DID.String()) 435 if err != nil { 436 + l.Error("failed to get bsky profile", "err", err) 437 w.WriteHeader(http.StatusNotFound) 438 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 439 return ··· 445 } 446 page, err := strconv.ParseInt(pageStr, 10, 64) 447 if err != nil { 448 + l.Error("failed to parse page value", "err", err) 449 page = 1 450 } 451 if page == 0 { ··· 457 458 sessions, err := db.GetStudySessionLogs(h.Db, ident.DID.String(), pageSize+1, int(offset)) 459 if err != nil { 460 + l.Error("failed to get study sessions", "err", err) 461 w.WriteHeader(http.StatusNotFound) 462 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 463 return ··· 465 466 sessions, err = ApplyPendingChanges(h, w, r, sessions, PendingStudySessionCreation, PendingStudySessionUpdates, PendingStudySessionDeletion) 467 if err != nil { 468 + l.Error("failed to save yoten-session after processing pending changes", "err", err) 469 } 470 471 feed := []*db.StudySessionFeedItem{} ··· 503 } 504 505 func (h *Handler) HandleFriendsPage(w http.ResponseWriter, r *http.Request) { 506 + l := h.Logger.With("handler", "HandleFriendsPage") 507 + 508 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 509 if err != nil { 510 + l.Error("failed to get logged-in user", "err", err) 511 htmx.HxRedirect(w, "/login") 512 return 513 } 514 515 followers, following, err := db.GetFollowerFollowingCount(h.Db, user.Did) 516 if err != nil { 517 + l.Error("failed to get follow stats", "err", err) 518 } 519 520 views.FriendsPage(views.FriendsPageParams{ ··· 525 } 526 527 func (h *Handler) HandleFriendsFeed(w http.ResponseWriter, r *http.Request) { 528 + l := h.Logger.With("handler", "HandleFriendsFeed") 529 + 530 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 531 if err != nil { 532 + l.Error("failed to get logged-in user") 533 htmx.HxRedirect(w, "/login") 534 return 535 } ··· 543 } 544 page, err := strconv.ParseInt(pageStr, 10, 64) 545 if err != nil { 546 + l.Error("failed to parse page value", "err", err) 547 page = 1 548 } 549 if page == 0 { ··· 559 if mode == partials.Following { 560 feed, err := db.GetFollowing(h.Db, user.Did, pageSize+1, int(offset)) 561 if err != nil { 562 + l.Error("failed to get following list", "err", err) 563 htmx.HxError(w, http.StatusInternalServerError, "Failed to get following list, try again later.") 564 return 565 } 566 bskyHydratedFeed, err = h.GetHydratedFollowerProfiles(feed) 567 if err != nil { 568 + l.Error("failed to hydrate bsky profiles", "err", err) 569 htmx.HxError(w, http.StatusInternalServerError, "Failed to get following list, try again later.") 570 return 571 } 572 } else { 573 feed, err := db.GetFollowers(h.Db, user.Did, pageSize+1, int(offset)) 574 if err != nil { 575 + l.Error("failed to get followers list", "err", err) 576 htmx.HxError(w, http.StatusInternalServerError, "Failed to get followers list, try again later.") 577 return 578 } 579 bskyHydratedFeed, err = h.GetHydratedFollowerProfiles(feed) 580 if err != nil { 581 + l.Error("failed to hydrate bsky profiles", "err", err) 582 htmx.HxError(w, http.StatusInternalServerError, "Failed to get following list, try again later.") 583 return 584 } ··· 600 } 601 602 func (h *Handler) HandleNotificationsPage(w http.ResponseWriter, r *http.Request) { 603 + l := h.Logger.With("handler", "HandleNotificationsPage") 604 + 605 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 606 if err != nil { 607 + l.Error("failed to get logged-in user", "err", err) 608 htmx.HxRedirect(w, "/login") 609 return 610 }
+17 -17
internal/server/handlers/reaction.go
··· 1 package handlers 2 3 import ( 4 - "log" 5 "net/http" 6 "slices" 7 "strconv" ··· 21 ) 22 23 func (h *Handler) HandleReaction(w http.ResponseWriter, r *http.Request) { 24 client, err := h.Oauth.AuthorizedClient(r) 25 if err != nil { 26 - log.Println("failed to get authorized client:", err) 27 htmx.HxRedirect(w, "/login") 28 return 29 } 30 31 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 32 if err != nil { 33 - log.Println("failed to get logged-in user:", err) 34 htmx.HxRedirect(w, "/login") 35 return 36 } ··· 53 } 54 55 if user.Did == session.Did { 56 - log.Println("failed to react to study session, cannot react to your own study session") 57 htmx.HxError(w, http.StatusBadRequest, "You cannot react to your own study sessions.") 58 return 59 } ··· 73 74 reaction, err := db.ReactionFromString(db.ReactionType(reactionId).String()) 75 if err != nil { 76 - log.Printf("failed to get reaction types: %v", err) 77 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 78 return 79 } 80 81 reactionEvents, err := db.GetReactionsForSession(h.Db, subjectDid, subjectRkey) 82 if err != nil { 83 - log.Println("failed to get reactions for study session from db:", err) 84 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 85 return 86 } ··· 89 case http.MethodPost: 90 reactionEvent, err := db.GetReactionEvent(h.Db, user.Did, session, reaction.ID) 91 if err != nil { 92 - log.Println("failed to get reaction event from db:", err) 93 htmx.HxError(w, http.StatusInternalServerError, "Failed to add reaction, try again later.") 94 return 95 } 96 if reactionEvent != nil { 97 - log.Println("failed to add reaction, user already reacted") 98 htmx.HxError(w, http.StatusBadRequest, "You cannot react multiple times with the same reaction.") 99 return 100 } ··· 113 }}, 114 }) 115 if err != nil { 116 - log.Println("failed to create reaction record:", err) 117 htmx.HxError(w, http.StatusInternalServerError, "Failed to add reaction, try again later.") 118 return 119 } ··· 128 Set("session_rkey", subjectRkey), 129 }) 130 if err != nil { 131 - log.Println("failed to enqueue posthog event:", err) 132 } 133 } 134 ··· 153 case http.MethodDelete: 154 reactionEvent, err := db.GetReactionEvent(h.Db, user.Did, session, reaction.ID) 155 if err != nil { 156 - log.Println("failed to get reaction event from db:", err) 157 htmx.HxError(w, http.StatusInternalServerError, "Failed to remove reaction, try again later.") 158 return 159 } ··· 164 Rkey: reactionEvent.Rkey, 165 }) 166 if err != nil { 167 - log.Println("failed to delete reaction record:", err) 168 htmx.HxError(w, http.StatusInternalServerError, "Failed to remove reaction, try again later.") 169 return 170 } ··· 179 Set("session_rkey", subjectRkey), 180 }) 181 if err != nil { 182 - log.Println("failed to enqueue posthog event:", err) 183 } 184 } 185 ··· 188 }) 189 190 partials.NewReactions(partials.NewReactionsProps{ 191 - User: user, 192 - SessionRkey: subjectRkey, 193 - SessionDid: subjectDid, 194 - // Reactions: reactions, 195 ReactionEvents: reactionEvents, 196 }).Render(r.Context(), w) 197 }
··· 1 package handlers 2 3 import ( 4 "net/http" 5 "slices" 6 "strconv" ··· 20 ) 21 22 func (h *Handler) HandleReaction(w http.ResponseWriter, r *http.Request) { 23 + l := h.Logger.With("handler", "HandleReaction") 24 + 25 client, err := h.Oauth.AuthorizedClient(r) 26 if err != nil { 27 + l.Error("failed to get authorized client", "err", err) 28 htmx.HxRedirect(w, "/login") 29 return 30 } 31 32 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 33 if err != nil { 34 + l.Error("failed to get logged-in user", "err", err) 35 htmx.HxRedirect(w, "/login") 36 return 37 } ··· 54 } 55 56 if user.Did == session.Did { 57 + l.Error("failed to react to study session, cannot react to your own study session") 58 htmx.HxError(w, http.StatusBadRequest, "You cannot react to your own study sessions.") 59 return 60 } ··· 74 75 reaction, err := db.ReactionFromString(db.ReactionType(reactionId).String()) 76 if err != nil { 77 + l.Error("failed to get reaction types", "err", err) 78 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 79 return 80 } 81 82 reactionEvents, err := db.GetReactionsForSession(h.Db, subjectDid, subjectRkey) 83 if err != nil { 84 + l.Error("failed to get reactions for study session from db", "err", err) 85 htmx.HxError(w, http.StatusInternalServerError, "Failed to get global study session feed, try again later.") 86 return 87 } ··· 90 case http.MethodPost: 91 reactionEvent, err := db.GetReactionEvent(h.Db, user.Did, session, reaction.ID) 92 if err != nil { 93 + l.Error("failed to get reaction event from db", "err", err) 94 htmx.HxError(w, http.StatusInternalServerError, "Failed to add reaction, try again later.") 95 return 96 } 97 if reactionEvent != nil { 98 + l.Error("failed to add reaction, user already reacted") 99 htmx.HxError(w, http.StatusBadRequest, "You cannot react multiple times with the same reaction.") 100 return 101 } ··· 114 }}, 115 }) 116 if err != nil { 117 + l.Error("failed to create reaction record", "err", err) 118 htmx.HxError(w, http.StatusInternalServerError, "Failed to add reaction, try again later.") 119 return 120 } ··· 129 Set("session_rkey", subjectRkey), 130 }) 131 if err != nil { 132 + l.Error("failed to enqueue posthog event", "err", err) 133 } 134 } 135 ··· 154 case http.MethodDelete: 155 reactionEvent, err := db.GetReactionEvent(h.Db, user.Did, session, reaction.ID) 156 if err != nil { 157 + l.Error("failed to get reaction event from db", "err", err) 158 htmx.HxError(w, http.StatusInternalServerError, "Failed to remove reaction, try again later.") 159 return 160 } ··· 165 Rkey: reactionEvent.Rkey, 166 }) 167 if err != nil { 168 + l.Error("failed to delete reaction record", "err", err) 169 htmx.HxError(w, http.StatusInternalServerError, "Failed to remove reaction, try again later.") 170 return 171 } ··· 180 Set("session_rkey", subjectRkey), 181 }) 182 if err != nil { 183 + l.Error("failed to enqueue posthog event", "err", err) 184 } 185 } 186 ··· 189 }) 190 191 partials.NewReactions(partials.NewReactionsProps{ 192 + User: user, 193 + SessionRkey: subjectRkey, 194 + SessionDid: subjectDid, 195 ReactionEvents: reactionEvents, 196 }).Render(r.Context(), w) 197 }
+14 -11
internal/server/handlers/stats.go
··· 1 package handlers 2 3 import ( 4 - "log" 5 "net/http" 6 7 "yoten.app/internal/clients/bsky" ··· 12 ) 13 14 func (h *Handler) HandleTimePerGraphs(w http.ResponseWriter, r *http.Request) { 15 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 16 if err != nil { 17 - log.Println("failed to get logged-in user:", err) 18 htmx.HxRedirect(w, "/login") 19 return 20 } ··· 24 25 chartData, err := db.GetTimePerData(h.Db, user.Did, period) 26 if err != nil { 27 - log.Println("failed to get time per chart data:", err) 28 chartData = db.ChartsData{ 29 ActivityData: []db.ChartData{}, 30 CategoryData: []db.ChartData{}, ··· 38 } 39 40 func (h *Handler) HandleStatsPage(w http.ResponseWriter, r *http.Request) { 41 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 42 if err != nil { 43 - log.Println("failed to get logged-in user:", err) 44 htmx.HxRedirect(w, "/login") 45 return 46 } 47 48 totalStudyTime, err := db.GetTotalStudyTime(h.Db, user.Did) 49 if err != nil { 50 - log.Println("failed to get total study time:", err) 51 } 52 53 totalStudySessions, err := db.GetTotalStudySessions(h.Db, user.Did) 54 if err != nil { 55 - log.Println("failed to get total study study sessions:", err) 56 } 57 58 totalActiveDays, err := db.GetTotalActiveDays(h.Db, user.Did) 59 if err != nil { 60 - log.Println("failed to get total active days:", err) 61 } 62 63 streak, err := db.GetCurrentStreak(h.Db, user.Did) 64 if err != nil { 65 - log.Println("failed to get streak:", err) 66 } 67 68 heatmap, err := db.GetHeatmapData(h.Db, user.Did) 69 if err != nil { 70 - log.Println("failed to get heatmap data:", err) 71 } 72 73 inputOutputPercentage, err := db.GetInputOutputPercentage(h.Db, user.Did) 74 if err != nil { 75 - log.Println("failed to get input vs output data:", err) 76 } 77 78 languageSummary, err := db.GetLanguageSummary(h.Db, user.Did) 79 if err != nil { 80 - log.Println("failed to get language time summary:", err) 81 } 82 languageChartSegments := db.ConvertToDonutChartSegments(languageSummary) 83
··· 1 package handlers 2 3 import ( 4 "net/http" 5 6 "yoten.app/internal/clients/bsky" ··· 11 ) 12 13 func (h *Handler) HandleTimePerGraphs(w http.ResponseWriter, r *http.Request) { 14 + l := h.Logger.With("handler", "HandleTimePerGraphs") 15 + 16 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 17 if err != nil { 18 + l.Error("failed to get logged-in user", "err", err) 19 htmx.HxRedirect(w, "/login") 20 return 21 } ··· 25 26 chartData, err := db.GetTimePerData(h.Db, user.Did, period) 27 if err != nil { 28 + l.Error("failed to get time per chart data", "err", err) 29 chartData = db.ChartsData{ 30 ActivityData: []db.ChartData{}, 31 CategoryData: []db.ChartData{}, ··· 39 } 40 41 func (h *Handler) HandleStatsPage(w http.ResponseWriter, r *http.Request) { 42 + l := h.Logger.With("handler", "HandleStatsPage") 43 + 44 user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 45 if err != nil { 46 + l.Error("failed to get logged-in user", "err", err) 47 htmx.HxRedirect(w, "/login") 48 return 49 } 50 51 totalStudyTime, err := db.GetTotalStudyTime(h.Db, user.Did) 52 if err != nil { 53 + l.Error("failed to get total study time", "err", err) 54 } 55 56 totalStudySessions, err := db.GetTotalStudySessions(h.Db, user.Did) 57 if err != nil { 58 + l.Error("failed to get total study study sessions", "err", err) 59 } 60 61 totalActiveDays, err := db.GetTotalActiveDays(h.Db, user.Did) 62 if err != nil { 63 + l.Error("failed to get total active days", "err", err) 64 } 65 66 streak, err := db.GetCurrentStreak(h.Db, user.Did) 67 if err != nil { 68 + l.Error("failed to get streak", "err", err) 69 } 70 71 heatmap, err := db.GetHeatmapData(h.Db, user.Did) 72 if err != nil { 73 + l.Error("failed to get heatmap data", "err", err) 74 } 75 76 inputOutputPercentage, err := db.GetInputOutputPercentage(h.Db, user.Did) 77 if err != nil { 78 + l.Error("failed to get input vs output data", "err", err) 79 } 80 81 languageSummary, err := db.GetLanguageSummary(h.Db, user.Did) 82 if err != nil { 83 + l.Error("failed to get language time summary", "err", err) 84 } 85 languageChartSegments := db.ConvertToDonutChartSegments(languageSummary) 86
+30 -20
internal/consumer/ingester.go
··· 4 "context" 5 "encoding/json" 6 "fmt" 7 - "log" 8 "strings" 9 "time" 10 ··· 20 type Ingester struct { 21 Db db.DbWrapper 22 Config *config.Config 23 } 24 25 type processFunc func(ctx context.Context, e *models.Event) error ··· 35 } 36 }() 37 38 switch e.Kind { 39 case models.EventKindCommit: 40 switch e.Commit.Collection { 41 case yoten.ActorProfileNSID: 42 err = i.ingestProfile(e) 43 case yoten.FeedSessionNSID: 44 err = i.ingestStudySession(e) 45 case yoten.ActivityDefNSID: 46 err = i.ingestActivityDef(e) 47 case yoten.FeedResourceNSID: 48 err = i.ingestResource(e) 49 case yoten.GraphFollowNSID: 50 err = i.ingestFollow(e) 51 case yoten.FeedReactionNSID: 52 err = i.ingestReaction(e) 53 case yoten.FeedCommentNSID: 54 err = i.ingestComment(e) 55 } 56 } 57 if err != nil { 58 - log.Printf("failed to ingest event for collection %s: %v", e.Commit.Collection, err) 59 } 60 61 return nil ··· 129 return fmt.Errorf("failed to start transaction: %w", err) 130 } 131 132 - log.Printf("upserting profile '%s' from pds request", profile.Did) 133 err = db.UpsertProfile(tx, &profile) 134 if err != nil { 135 tx.Rollback() ··· 160 161 date, err := time.Parse(time.RFC3339, record.Date) 162 if err != nil { 163 - log.Printf("invalid record: %s", err) 164 return err 165 } 166 ··· 227 return fmt.Errorf("failed to start transaction: %w", err) 228 } 229 230 - log.Println("upserting study session from pds request") 231 err = db.UpsertStudySession(tx, &studySession, e.Commit.RKey) 232 if err != nil { 233 tx.Rollback() ··· 252 return fmt.Errorf("failed to start transaction: %w", err) 253 } 254 255 - log.Println("deleting study session from pds request") 256 err = db.DeleteStudySessionByRkey(tx, did, e.Commit.RKey) 257 if err != nil { 258 tx.Rollback() ··· 344 return fmt.Errorf("failed to start transaction: %w", err) 345 } 346 347 - log.Println("upserting activity def from pds request") 348 err = db.UpsertActivityDef(tx, &activityDef, e.Commit.RKey) 349 if err != nil { 350 tx.Rollback() ··· 352 } 353 return tx.Commit() 354 case models.CommitOperationDelete: 355 - log.Println("deleting activity def from pds request") 356 err = db.DeleteActivityDefByRkey(i.Db, did, e.Commit.RKey) 357 } 358 if err != nil { ··· 387 388 subjectDid := record.Subject 389 390 - log.Println("upserting follow from pds request") 391 err = db.AddFollow(tx, did, subjectDid, e.Commit.RKey) 392 if err != nil { 393 tx.Rollback() ··· 397 subjectUri := fmt.Sprintf("at://%s/%s/%s", did, yoten.GraphFollowNSID, e.Commit.RKey) 398 err = db.CreateNotification(tx, subjectDid, did, subjectUri, db.NotificationTypeFollow) 399 if err != nil { 400 - log.Println("failed to create notification record:", err) 401 } 402 403 return tx.Commit() 404 case models.CommitOperationDelete: 405 - log.Println("deleting follow from pds request") 406 err = db.DeleteFollowByRkey(i.Db, did, e.Commit.RKey) 407 } 408 if err != nil { ··· 465 CreatedAt: createdAt, 466 } 467 468 - log.Println("upserting reaction from pds request") 469 err = db.UpsertReaction(i.Db, reactionEvent) 470 if err != nil { 471 tx.Rollback() ··· 474 475 err = db.CreateNotification(tx, subjectDid.String(), did, subject.String(), db.NotificationTypeReaction) 476 if err != nil { 477 - log.Println("failed to create notification record:", err) 478 } 479 480 return tx.Commit() 481 case models.CommitOperationDelete: 482 - log.Println("deleting reaction from pds request") 483 err = db.DeleteReactionByRkey(i.Db, did, e.Commit.RKey) 484 } 485 if err != nil { ··· 546 return fmt.Errorf("invalid resource: %w", err) 547 } 548 549 - log.Println("upserting resource from pds request") 550 err = db.UpsertResource(i.Db, resource, resource.Rkey) 551 if err != nil { 552 tx.Rollback() ··· 554 } 555 return tx.Commit() 556 case models.CommitOperationDelete: 557 - log.Println("deleting resource from pds request") 558 err = db.DeleteResourceByRkey(i.Db, did, e.Commit.RKey) 559 } 560 if err != nil { ··· 626 CreatedAt: createdAt, 627 } 628 629 - log.Println("upserting comment from pds request") 630 err = db.UpsertComment(i.Db, comment) 631 if err != nil { 632 tx.Rollback() ··· 637 if subjectDid.String() != did { 638 err = db.CreateNotification(tx, subjectDid.String(), did, subjectUri.String(), db.NotificationTypeComment) 639 if err != nil { 640 - log.Println("failed to create notification record:", err) 641 } 642 } 643 ··· 645 if comment.ParentCommentUri != nil && comment.ParentCommentUri.Authority().String() != did { 646 err = db.CreateNotification(tx, comment.ParentCommentUri.Authority().String(), did, parentCommentUri.String(), db.NotificationTypeReply) 647 if err != nil { 648 - log.Println("failed to create notification record:", err) 649 } 650 } 651 652 return tx.Commit() 653 case models.CommitOperationDelete: 654 - log.Println("deleting comment from pds request") 655 err = db.DeleteCommentByRkey(i.Db, did, e.Commit.RKey) 656 } 657 if err != nil {
··· 4 "context" 5 "encoding/json" 6 "fmt" 7 + "log/slog" 8 "strings" 9 "time" 10 ··· 20 type Ingester struct { 21 Db db.DbWrapper 22 Config *config.Config 23 + Logger *slog.Logger 24 } 25 26 type processFunc func(ctx context.Context, e *models.Event) error ··· 36 } 37 }() 38 39 + l := i.Logger.With("kind", e.Kind) 40 switch e.Kind { 41 case models.EventKindCommit: 42 switch e.Commit.Collection { 43 case yoten.ActorProfileNSID: 44 + l = l.With("handler", "ingestProfile") 45 err = i.ingestProfile(e) 46 case yoten.FeedSessionNSID: 47 + l = l.With("handler", "ingestStudySession") 48 err = i.ingestStudySession(e) 49 case yoten.ActivityDefNSID: 50 + l = l.With("handler", "ingestActivityDef") 51 err = i.ingestActivityDef(e) 52 case yoten.FeedResourceNSID: 53 + l = l.With("handler", "ingestResource") 54 err = i.ingestResource(e) 55 case yoten.GraphFollowNSID: 56 + l = l.With("handler", "ingestFollow") 57 err = i.ingestFollow(e) 58 case yoten.FeedReactionNSID: 59 + l = l.With("handler", "ingestReaction") 60 err = i.ingestReaction(e) 61 case yoten.FeedCommentNSID: 62 + l = l.With("handler", "ingestComment") 63 err = i.ingestComment(e) 64 } 65 + l = i.Logger.With("nsid", e.Commit.Collection) 66 } 67 if err != nil { 68 + l.Error("failed to ingest event", "err", err) 69 } 70 71 return nil ··· 139 return fmt.Errorf("failed to start transaction: %w", err) 140 } 141 142 + i.Logger.Debug("upserting profile from pds request") 143 err = db.UpsertProfile(tx, &profile) 144 if err != nil { 145 tx.Rollback() ··· 170 171 date, err := time.Parse(time.RFC3339, record.Date) 172 if err != nil { 173 + i.Logger.Error("invalid record", "err", err) 174 return err 175 } 176 ··· 237 return fmt.Errorf("failed to start transaction: %w", err) 238 } 239 240 + i.Logger.Debug("upserting study session from pds request") 241 err = db.UpsertStudySession(tx, &studySession, e.Commit.RKey) 242 if err != nil { 243 tx.Rollback() ··· 262 return fmt.Errorf("failed to start transaction: %w", err) 263 } 264 265 + i.Logger.Debug("deleting study session from pds request") 266 err = db.DeleteStudySessionByRkey(tx, did, e.Commit.RKey) 267 if err != nil { 268 tx.Rollback() ··· 354 return fmt.Errorf("failed to start transaction: %w", err) 355 } 356 357 + i.Logger.Debug("upserting activity def from pds request") 358 err = db.UpsertActivityDef(tx, &activityDef, e.Commit.RKey) 359 if err != nil { 360 tx.Rollback() ··· 362 } 363 return tx.Commit() 364 case models.CommitOperationDelete: 365 + i.Logger.Debug("deleting activity def from pds request") 366 err = db.DeleteActivityDefByRkey(i.Db, did, e.Commit.RKey) 367 } 368 if err != nil { ··· 397 398 subjectDid := record.Subject 399 400 + i.Logger.Debug("upserting follow from pds request") 401 err = db.AddFollow(tx, did, subjectDid, e.Commit.RKey) 402 if err != nil { 403 tx.Rollback() ··· 407 subjectUri := fmt.Sprintf("at://%s/%s/%s", did, yoten.GraphFollowNSID, e.Commit.RKey) 408 err = db.CreateNotification(tx, subjectDid, did, subjectUri, db.NotificationTypeFollow) 409 if err != nil { 410 + i.Logger.Error("failed to create notification record", "err", err) 411 } 412 413 return tx.Commit() 414 case models.CommitOperationDelete: 415 + i.Logger.Debug("deleting follow from pds request") 416 err = db.DeleteFollowByRkey(i.Db, did, e.Commit.RKey) 417 } 418 if err != nil { ··· 475 CreatedAt: createdAt, 476 } 477 478 + i.Logger.Debug("upserting reaction from pds request") 479 err = db.UpsertReaction(i.Db, reactionEvent) 480 if err != nil { 481 tx.Rollback() ··· 484 485 err = db.CreateNotification(tx, subjectDid.String(), did, subject.String(), db.NotificationTypeReaction) 486 if err != nil { 487 + i.Logger.Error("failed to create notification record", "err", err) 488 } 489 490 return tx.Commit() 491 case models.CommitOperationDelete: 492 + i.Logger.Debug("deleting reaction from pds request") 493 err = db.DeleteReactionByRkey(i.Db, did, e.Commit.RKey) 494 } 495 if err != nil { ··· 556 return fmt.Errorf("invalid resource: %w", err) 557 } 558 559 + i.Logger.Debug("upserting resource from pds request") 560 err = db.UpsertResource(i.Db, resource, resource.Rkey) 561 if err != nil { 562 tx.Rollback() ··· 564 } 565 return tx.Commit() 566 case models.CommitOperationDelete: 567 + i.Logger.Debug("deleting resource from pds request") 568 err = db.DeleteResourceByRkey(i.Db, did, e.Commit.RKey) 569 } 570 if err != nil { ··· 636 CreatedAt: createdAt, 637 } 638 639 + i.Logger.Debug("upserting comment from pds request") 640 err = db.UpsertComment(i.Db, comment) 641 if err != nil { 642 tx.Rollback() ··· 647 if subjectDid.String() != did { 648 err = db.CreateNotification(tx, subjectDid.String(), did, subjectUri.String(), db.NotificationTypeComment) 649 if err != nil { 650 + i.Logger.Error("failed to create notification record", "err", err) 651 } 652 } 653 ··· 655 if comment.ParentCommentUri != nil && comment.ParentCommentUri.Authority().String() != did { 656 err = db.CreateNotification(tx, comment.ParentCommentUri.Authority().String(), did, parentCommentUri.String(), db.NotificationTypeReply) 657 if err != nil { 658 + i.Logger.Error("failed to create notification record", "err", err) 659 } 660 } 661 662 return tx.Commit() 663 case models.CommitOperationDelete: 664 + i.Logger.Debug("deleting comment from pds request") 665 err = db.DeleteCommentByRkey(i.Db, did, e.Commit.RKey) 666 } 667 if err != nil {
+9 -7
internal/server/app.go
··· 61 62 63 64 65 66 67 ··· 77 78 79 80 - 81 - 82 - 83 - 84 - 85 yoten.GraphFollowNSID, 86 }, 87 nil, 88 - slog.Default(), 89 wrapper, 90 false, 91 ) ··· 95 96 ingester := consumer.Ingester{ 97 Db: wrapper, 98 - Config: config} 99 err = jc.StartJetstream(ctx, ingester.Ingest()) 100 if err != nil { 101 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
··· 61 62 63 64 + idResolver := atproto.DefaultResolver() 65 66 + oauth, err := oauth.New(config, posthog, idResolver, log.SubLogger(logger, "oauth")) 67 + if err != nil { 68 + return nil, fmt.Errorf("failed to start oauth handler: %w", err) 69 + } 70 71 72 ··· 82 83 84 85 yoten.GraphFollowNSID, 86 }, 87 nil, 88 + log.SubLogger(logger, "jetstream"), 89 wrapper, 90 false, 91 ) ··· 95 96 ingester := consumer.Ingester{ 97 Db: wrapper, 98 + Config: config, 99 + Logger: log.SubLogger(logger, "ingester"), 100 + } 101 err = jc.StartJetstream(ctx, ingester.Ingest()) 102 if err != nil { 103 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
+2
internal/server/handlers/router.go
··· 7 "github.com/go-chi/chi/v5" 8 9 "yoten.app/internal/server" 10 "yoten.app/internal/server/middleware" 11 "yoten.app/internal/server/views" 12 ) ··· 25 h.Oauth, 26 h.Db, 27 h.IdResolver, 28 ) 29 30 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
··· 7 "github.com/go-chi/chi/v5" 8 9 "yoten.app/internal/server" 10 + "yoten.app/internal/server/log" 11 "yoten.app/internal/server/middleware" 12 "yoten.app/internal/server/views" 13 ) ··· 26 h.Oauth, 27 h.Db, 28 h.IdResolver, 29 + log.SubLogger(h.Logger, "middleware"), 30 ) 31 32 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
+13 -6
internal/server/middleware/middleware.go
··· 3 import ( 4 "context" 5 "fmt" 6 - "log" 7 "net/http" 8 "net/url" 9 "slices" ··· 25 oauth *oauth.OAuth 26 db *db.DB 27 idResolver *atproto.Resolver 28 } 29 30 - func New(oauth *oauth.OAuth, db *db.DB, idResolver *atproto.Resolver) Middleware { 31 return Middleware{ 32 oauth: oauth, 33 db: db, 34 idResolver: idResolver, 35 } 36 } 37 38 type middlewareFunc func(http.Handler) http.Handler 39 40 func AuthMiddleware(o *oauth.OAuth) middlewareFunc { 41 return func(next http.Handler) http.Handler { 42 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 returnURL := "/" ··· 59 60 sess, err := o.ResumeSession(r) 61 if err != nil { 62 - log.Println("failed to resume session, redirecting...", "err", err, "url", r.URL.String()) 63 redirectFunc(w, r) 64 return 65 } 66 67 if sess == nil { 68 - log.Printf("session is nil, redirecting...") 69 redirectFunc(w, r) 70 return 71 } ··· 76 } 77 78 func (mw Middleware) ResolveIdent() middlewareFunc { 79 excluded := []string{"favicon.ico"} 80 81 return func(next http.Handler) http.Handler { ··· 90 91 id, err := mw.idResolver.ResolveIdent(r.Context(), didOrHandle) 92 if err != nil { 93 - log.Println("failed to resolve did/handle:", err) 94 w.WriteHeader(http.StatusNotFound) 95 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 96 return ··· 104 } 105 106 func (mw Middleware) LoadUnreadNotificationCount() middlewareFunc { 107 return func(next http.Handler) http.Handler { 108 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 109 user := mw.oauth.GetUser(r) ··· 114 115 count, err := db.GetUnreadNotificationCount(mw.db, user.Did) 116 if err != nil { 117 - log.Println("failed to get notification count:", err) 118 } 119 120 ctx := context.WithValue(r.Context(), UnreadNotificationCountCtxKey, count)
··· 3 import ( 4 "context" 5 "fmt" 6 + "log/slog" 7 "net/http" 8 "net/url" 9 "slices" ··· 25 oauth *oauth.OAuth 26 db *db.DB 27 idResolver *atproto.Resolver 28 + logger *slog.Logger 29 } 30 31 + func New(oauth *oauth.OAuth, db *db.DB, idResolver *atproto.Resolver, logger *slog.Logger) Middleware { 32 return Middleware{ 33 oauth: oauth, 34 db: db, 35 idResolver: idResolver, 36 + logger: logger, 37 } 38 } 39 40 type middlewareFunc func(http.Handler) http.Handler 41 42 func AuthMiddleware(o *oauth.OAuth) middlewareFunc { 43 + l := o.Logger.With("middleware", "AuthMiddleware") 44 + 45 return func(next http.Handler) http.Handler { 46 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 47 returnURL := "/" ··· 63 64 sess, err := o.ResumeSession(r) 65 if err != nil { 66 + l.Error("failed to resume session, redirecting...", "err", err, "url", r.URL.String()) 67 redirectFunc(w, r) 68 return 69 } 70 71 if sess == nil { 72 + l.Warn("session is nil, redirecting...") 73 redirectFunc(w, r) 74 return 75 } ··· 80 } 81 82 func (mw Middleware) ResolveIdent() middlewareFunc { 83 + l := mw.logger.With("middleware", "ResolveIdent") 84 excluded := []string{"favicon.ico"} 85 86 return func(next http.Handler) http.Handler { ··· 95 96 id, err := mw.idResolver.ResolveIdent(r.Context(), didOrHandle) 97 if err != nil { 98 + l.Error("failed to resolve did/handle", "err", err) 99 w.WriteHeader(http.StatusNotFound) 100 views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 101 return ··· 109 } 110 111 func (mw Middleware) LoadUnreadNotificationCount() middlewareFunc { 112 + l := mw.logger.With("middleware", "LoadUnreadNotificationCount") 113 + 114 return func(next http.Handler) http.Handler { 115 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 116 user := mw.oauth.GetUser(r) ··· 121 122 count, err := db.GetUnreadNotificationCount(mw.db, user.Did) 123 if err != nil { 124 + l.Error("failed to get notification count", "err", err) 125 } 126 127 ctx := context.WithValue(r.Context(), UnreadNotificationCountCtxKey, count)

History

2 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
fix: accounts not created on sign-in
expand 0 comments
pull request successfully merged
brookjeynes.dev submitted #0
18 commits
expand
refactor: move computed data code to function to simplify visuals
feat: use charmbracelet logger
feat(oauth): use slogger
feat(db): use slogger
feat(handlers/study-session): use slogger
feat(handlers/resource): use slogger
feat(handlers/comment): use slogger
feat(handlers/follow): use slogger
feat(handlers/activity): use slogger
feat(handlers/login): use slogger
feat(handlers/notification): use slogger
feat(handlers/profile): use slogger
feat(handlers/reaction): use slogger
feat(handlers/stats): use slogger
feat(consumer/ingester): use slogger
feat(middleware): use slogger
fix(auth): handle bad identity lookups
fix: accounts not created on sign-in
expand 0 comments