Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

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

refactor: use atp library

+257 -808
+21 -20
go.mod
··· 1 1 module arabica 2 2 3 - go 1.25.4 3 + go 1.25.5 4 4 5 5 require ( 6 6 github.com/XSAM/otelsql v0.41.0 7 7 github.com/a-h/templ v0.3.1001 8 - github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725 8 + github.com/bluesky-social/indigo v0.0.0-20260318212431-cbaa83aee9dd 9 9 github.com/go-logr/zerologr v1.2.3 10 10 github.com/google/go-querystring v1.1.0 11 11 github.com/gorilla/websocket v1.5.3 ··· 14 14 github.com/prometheus/client_model v0.6.2 15 15 github.com/rs/zerolog v1.34.0 16 16 github.com/stretchr/testify v1.11.1 17 - go.etcd.io/bbolt v1.3.8 17 + go.etcd.io/bbolt v1.4.3 18 18 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 19 - go.opentelemetry.io/otel v1.40.0 20 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 21 - go.opentelemetry.io/otel/sdk v1.40.0 22 - go.opentelemetry.io/otel/trace v1.40.0 19 + go.opentelemetry.io/otel v1.43.0 20 + go.opentelemetry.io/otel/sdk v1.43.0 21 + go.opentelemetry.io/otel/trace v1.43.0 23 22 golang.org/x/image v0.38.0 24 23 golang.org/x/sync v0.20.0 25 - modernc.org/sqlite v1.46.1 24 + modernc.org/sqlite v1.48.1 25 + tangled.org/pdewey.com/atp v0.0.0-20260407015143-f53954e5e783 26 26 ) 27 27 28 28 require ( ··· 38 38 github.com/gogo/protobuf v1.3.2 // indirect 39 39 github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 40 40 github.com/google/uuid v1.6.0 // indirect 41 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect 41 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect 42 42 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 43 43 github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 44 44 github.com/hashicorp/golang-lru v1.0.2 // indirect ··· 69 69 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 70 70 github.com/ncruces/go-strftime v1.0.0 // indirect 71 71 github.com/opentracing/opentracing-go v1.2.0 // indirect 72 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 72 73 github.com/pmezard/go-difflib v1.0.0 // indirect 73 74 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 74 75 github.com/prometheus/common v0.66.1 // indirect ··· 79 80 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 80 81 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 81 82 go.opentelemetry.io/auto/sdk v1.2.1 // indirect 82 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect 83 - go.opentelemetry.io/otel/metric v1.40.0 // indirect 84 - go.opentelemetry.io/proto/otlp v1.9.0 // indirect 83 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect 84 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect 85 + go.opentelemetry.io/otel/metric v1.43.0 // indirect 86 + go.opentelemetry.io/proto/otlp v1.10.0 // indirect 85 87 go.uber.org/atomic v1.11.0 // indirect 86 88 go.uber.org/multierr v1.11.0 // indirect 87 89 go.uber.org/zap v1.26.0 // indirect 88 90 go.yaml.in/yaml/v2 v2.4.2 // indirect 89 - golang.org/x/crypto v0.47.0 // indirect 90 - golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect 91 - golang.org/x/net v0.49.0 // indirect 92 - golang.org/x/sys v0.40.0 // indirect 91 + golang.org/x/crypto v0.49.0 // indirect 92 + golang.org/x/net v0.52.0 // indirect 93 + golang.org/x/sys v0.42.0 // indirect 93 94 golang.org/x/text v0.35.0 // indirect 94 95 golang.org/x/time v0.3.0 // indirect 95 96 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 96 - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect 97 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect 98 - google.golang.org/grpc v1.78.0 // indirect 97 + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect 98 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect 99 + google.golang.org/grpc v1.80.0 // indirect 99 100 google.golang.org/protobuf v1.36.11 // indirect 100 101 gopkg.in/yaml.v3 v3.0.1 // indirect 101 102 lukechampine.com/blake3 v1.2.1 // indirect 102 - modernc.org/libc v1.67.6 // indirect 103 + modernc.org/libc v1.70.0 // indirect 103 104 modernc.org/mathutil v1.7.1 // indirect 104 105 modernc.org/memory v1.11.0 // indirect 105 106 )
+53 -48
go.sum
··· 6 6 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 7 7 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 8 8 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 9 - github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725 h1:gfrLAhE6PHun4MDypO/5hpnaHPd9Dbe9+JxZL0gC4ic= 10 - github.com/bluesky-social/indigo v0.0.0-20260106221649-6fcd9317e725/go.mod h1:KIy0FgNQacp4uv2Z7xhNkV3qZiUSGuRky97s7Pa4v+o= 9 + github.com/bluesky-social/indigo v0.0.0-20260318212431-cbaa83aee9dd h1:FZSMlxClfm7jCA6A/vwTNw5EPxSngPPpK09MxuEx9l0= 10 + github.com/bluesky-social/indigo v0.0.0-20260318212431-cbaa83aee9dd/go.mod h1:VG/LeqLGNI3Ew7lsYixajnZGFfWPv144qbUddh+Oyag= 11 11 github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 12 12 github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 13 13 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= ··· 52 52 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 53 53 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 54 54 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 55 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= 56 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= 55 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= 56 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= 57 57 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 58 58 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 59 59 github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= ··· 138 138 github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 139 139 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 140 140 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 141 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 142 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 141 143 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 142 144 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 143 145 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= ··· 187 189 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 188 190 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 189 191 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 190 - go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= 191 - go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= 192 + go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= 193 + go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= 192 194 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 193 195 go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 194 196 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= 195 197 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= 196 - go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= 197 - go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= 198 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= 199 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= 200 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= 201 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= 202 - go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= 203 - go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= 204 - go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= 205 - go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= 206 - go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= 207 - go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= 208 - go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= 209 - go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= 210 - go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= 211 - go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= 198 + go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= 199 + go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= 200 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= 201 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= 202 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= 203 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= 204 + go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= 205 + go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= 206 + go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= 207 + go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= 208 + go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= 209 + go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= 210 + go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= 211 + go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= 212 + go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= 213 + go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= 212 214 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 213 215 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 214 216 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 231 233 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 232 234 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 233 235 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 234 - golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= 235 - golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= 236 - golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= 237 - golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 236 + golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= 237 + golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= 238 238 golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= 239 239 golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= 240 240 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= ··· 250 250 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 251 251 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 252 252 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 253 - golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= 254 - golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= 253 + golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= 254 + golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= 255 255 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 256 256 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 257 257 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 266 266 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 267 267 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 268 268 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 269 + golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 269 270 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 270 271 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 271 272 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 272 - golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= 273 - golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 273 + golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= 274 + golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 274 275 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 275 276 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 276 277 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= ··· 296 297 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 297 298 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 298 299 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 299 - gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 300 - gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 301 - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= 302 - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= 303 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= 304 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= 305 - google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= 306 - google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= 300 + gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= 301 + gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= 302 + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= 303 + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= 304 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= 305 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= 306 + google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= 307 + google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= 307 308 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 308 309 google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 309 310 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= ··· 322 323 lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= 323 324 modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= 324 325 modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 325 - modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= 326 - modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= 327 - modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= 328 - modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= 326 + modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= 327 + modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= 328 + modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= 329 + modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= 329 330 modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= 330 331 modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 331 - modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= 332 - modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= 332 + modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= 333 + modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= 333 334 modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= 334 335 modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= 335 - modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= 336 - modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= 336 + modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= 337 + modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= 337 338 modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 338 339 modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 339 340 modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= ··· 342 343 modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 343 344 modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= 344 345 modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= 345 - modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= 346 - modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= 346 + modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA= 347 + modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= 347 348 modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 348 349 modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 349 350 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 350 351 modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 352 + tangled.org/pdewey.com/atp v0.0.0-20260406012022-a83263945099 h1:9qpnxJ8bSEsIycH9ySVg3mRh9nCFts1LZrMSWIzKMsA= 353 + tangled.org/pdewey.com/atp v0.0.0-20260406012022-a83263945099/go.mod h1:+ayXDZgZZpb3UF2how+7BmPGLsD3JVJjCzhCk2lrv4o= 354 + tangled.org/pdewey.com/atp v0.0.0-20260407015143-f53954e5e783 h1:2ZJGx2rhmISlDIgcLSmtjt/PKbI3LcGIx3jqZL6o3wE= 355 + tangled.org/pdewey.com/atp v0.0.0-20260407015143-f53954e5e783/go.mod h1:+ayXDZgZZpb3UF2how+7BmPGLsD3JVJjCzhCk2lrv4o=
+103 -321
internal/atproto/client.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "errors" 6 5 "fmt" 7 6 "net/http" 8 - "strings" 9 7 10 8 "arabica/internal/metrics" 11 9 "arabica/internal/tracing" 12 10 13 - "github.com/bluesky-social/indigo/atproto/atclient" 14 11 "github.com/bluesky-social/indigo/atproto/syntax" 15 12 "github.com/rs/zerolog/log" 16 13 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 14 + "tangled.org/pdewey.com/atp" 17 15 ) 18 16 19 - // ErrSessionExpired is returned when the OAuth session cannot be resumed, 20 - // indicating the user's authorization grant has expired and they need to log in again. 21 - var ErrSessionExpired = errors.New("oauth session expired") 17 + // ErrSessionExpired is returned when the OAuth session cannot be resumed. 18 + var ErrSessionExpired = atp.ErrSessionExpired 22 19 23 - // wrapPDSError checks whether an error from an XRPC call indicates that the 24 - // OAuth grant is no longer valid (e.g. token refresh returned invalid_grant) 25 - // and, if so, wraps it with ErrSessionExpired so that upstream handlers can 26 - // return 401 instead of 500. 27 - func wrapPDSError(err error) error { 28 - if err == nil { 29 - return nil 30 - } 31 - msg := err.Error() 32 - if strings.Contains(msg, "invalid_grant") || 33 - strings.Contains(msg, "failed to refresh OAuth tokens") || 34 - strings.Contains(msg, "token is expired") { 35 - return fmt.Errorf("%w: %w", ErrSessionExpired, err) 36 - } 37 - return err 38 - } 20 + // wrapPDSError checks whether an error indicates an expired OAuth grant. 21 + var wrapPDSError = atp.WrapPDSError 22 + 23 + // Record represents a single record from a PDS. 24 + type Record = atp.Record 39 25 40 - // Client wraps the atproto API client for making authenticated requests to a PDS 26 + // Client wraps the atproto API client for making authenticated requests to a PDS. 41 27 type Client struct { 42 28 oauth *OAuthManager 43 29 } 44 30 45 - // NewClient creates a new atproto client 31 + // NewClient creates a new atproto client. 46 32 func NewClient(oauth *OAuthManager) *Client { 47 - return &Client{ 48 - oauth: oauth, 49 - } 33 + return &Client{oauth: oauth} 50 34 } 51 35 52 - // getAuthenticatedAPIClient creates an authenticated API client for a specific session 53 - // This properly handles DPOP token signing and refresh 54 - func (c *Client) getAuthenticatedAPIClient(ctx context.Context, did syntax.DID, sessionID string) (*atclient.APIClient, error) { 55 - // Resume the OAuth session - this returns a ClientSession that handles DPOP 36 + // getAtpClient resumes an OAuth session and returns an atp.Client with OTel-instrumented transport. 37 + func (c *Client) getAtpClient(ctx context.Context, did syntax.DID, sessionID string) (*atp.Client, error) { 56 38 session, err := c.oauth.app.ResumeSession(ctx, did, sessionID) 57 39 if err != nil { 58 40 return nil, fmt.Errorf("%w: %w", ErrSessionExpired, err) 59 41 } 60 42 61 - // Get the authenticated API client from the session 62 - // This client automatically handles DPOP signing and token refresh 63 43 apiClient := session.APIClient() 64 44 65 - // Wrap the HTTP transport with OpenTelemetry instrumentation so outbound PDS 66 - // HTTP calls appear as child spans under the existing pds.* semantic spans. 67 - // We create a new http.Client rather than mutating the shared session client. 45 + // Wrap transport with OTel instrumentation. 68 46 baseTransport := apiClient.Client.Transport 69 47 if baseTransport == nil { 70 48 baseTransport = http.DefaultTransport ··· 76 54 Jar: apiClient.Client.Jar, 77 55 } 78 56 79 - return apiClient, nil 57 + return atp.NewClient(apiClient, did), nil 80 58 } 81 59 82 - // CreateRecordInput contains parameters for creating a record 60 + // --- Input/Output types (kept for caller compatibility) --- 61 + 83 62 type CreateRecordInput struct { 84 63 Collection string 85 64 Record any 86 - RKey *string // Optional, if nil a TID will be generated 65 + RKey *string 87 66 } 88 67 89 - // CreateRecordOutput contains the result of creating a record 90 68 type CreateRecordOutput struct { 91 - URI string // AT-URI of the created record 92 - CID string // Content ID 69 + URI string 70 + CID string 71 + } 72 + 73 + type GetRecordInput struct { 74 + Collection string 75 + RKey string 76 + } 77 + 78 + type GetRecordOutput struct { 79 + URI string 80 + CID string 81 + Value map[string]any 82 + } 83 + 84 + type ListRecordsInput struct { 85 + Collection string 86 + Limit *int64 87 + Cursor *string 88 + } 89 + 90 + type ListRecordsOutput struct { 91 + Records []Record 92 + Cursor *string 93 + } 94 + 95 + type PutRecordInput struct { 96 + Collection string 97 + RKey string 98 + Record any 93 99 } 94 100 95 - // CreateRecord creates a new record in the user's repository 101 + type DeleteRecordInput struct { 102 + Collection string 103 + RKey string 104 + } 105 + 106 + // --- CRUD methods --- 107 + 96 108 func (c *Client) CreateRecord(ctx context.Context, did syntax.DID, sessionID string, input *CreateRecordInput) (*CreateRecordOutput, error) { 97 109 ctx, span := tracing.PdsSpan(ctx, "createRecord", input.Collection, did.String()) 98 110 defer span.End() 99 111 100 - apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 112 + atpClient, err := c.getAtpClient(ctx, did, sessionID) 101 113 if err != nil { 102 114 tracing.EndWithError(span, err) 103 115 return nil, err 104 116 } 105 117 106 - // Build the request body 107 - body := map[string]any{ 108 - "repo": did.String(), 109 - "collection": input.Collection, 110 - "record": input.Record, 111 - } 112 - 118 + var uri, cid string 113 119 if input.RKey != nil { 114 - body["rkey"] = *input.RKey 115 - } 116 - 117 - // Use the API client's Post method to call com.atproto.repo.createRecord 118 - var result struct { 119 - URI string `json:"uri"` 120 - CID string `json:"cid"` 120 + uri, cid, err = atpClient.CreateRecordWithRKey(ctx, input.Collection, *input.RKey, input.Record) 121 + } else { 122 + uri, cid, err = atpClient.CreateRecord(ctx, input.Collection, input.Record) 121 123 } 122 - 123 - err = apiClient.Post(ctx, "com.atproto.repo.createRecord", body, &result) 124 124 metrics.PDSRequestsTotal.WithLabelValues("createRecord", input.Collection).Inc() 125 125 126 126 if err != nil { 127 - err = wrapPDSError(err) 128 127 tracing.EndWithError(span, err) 129 - log.Error(). 130 - Err(err). 131 - Str("method", "createRecord"). 132 - Str("collection", input.Collection). 133 - Str("did", did.String()). 134 - Msg("PDS request failed") 128 + log.Error().Err(err).Str("method", "createRecord").Str("collection", input.Collection).Str("did", did.String()).Msg("PDS request failed") 135 129 return nil, fmt.Errorf("failed to create record: %w", err) 136 130 } 137 131 138 - log.Debug(). 139 - Str("method", "createRecord"). 140 - Str("collection", input.Collection). 141 - Str("did", did.String()). 142 - Str("uri", result.URI). 143 - Str("cid", result.CID). 144 - Msg("PDS request completed") 145 - 146 - return &CreateRecordOutput{ 147 - URI: result.URI, 148 - CID: result.CID, 149 - }, nil 132 + log.Debug().Str("method", "createRecord").Str("collection", input.Collection).Str("did", did.String()).Str("uri", uri).Str("cid", cid).Msg("PDS request completed") 133 + return &CreateRecordOutput{URI: uri, CID: cid}, nil 150 134 } 151 135 152 - // GetRecordInput contains parameters for getting a record 153 - type GetRecordInput struct { 154 - Collection string 155 - RKey string 156 - } 157 - 158 - // GetRecordOutput contains the result of getting a record 159 - type GetRecordOutput struct { 160 - URI string 161 - CID string 162 - Value map[string]any 163 - } 164 - 165 - // GetRecord retrieves a single record by its rkey 166 136 func (c *Client) GetRecord(ctx context.Context, did syntax.DID, sessionID string, input *GetRecordInput) (*GetRecordOutput, error) { 167 137 ctx, span := tracing.PdsSpan(ctx, "getRecord", input.Collection, did.String()) 168 138 defer span.End() 169 139 170 - apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 140 + atpClient, err := c.getAtpClient(ctx, did, sessionID) 171 141 if err != nil { 172 142 tracing.EndWithError(span, err) 173 143 return nil, err 174 144 } 175 145 176 - // Build query parameters 177 - params := map[string]any{ 178 - "repo": did.String(), 179 - "collection": input.Collection, 180 - "rkey": input.RKey, 181 - } 182 - 183 - // Use the API client's Get method to call com.atproto.repo.getRecord 184 - var result struct { 185 - URI string `json:"uri"` 186 - CID string `json:"cid"` 187 - Value map[string]any `json:"value"` 188 - } 189 - 190 - err = apiClient.Get(ctx, "com.atproto.repo.getRecord", params, &result) 146 + rec, err := atpClient.GetRecord(ctx, input.Collection, input.RKey) 191 147 metrics.PDSRequestsTotal.WithLabelValues("getRecord", input.Collection).Inc() 192 148 193 149 if err != nil { 194 - err = wrapPDSError(err) 195 150 tracing.EndWithError(span, err) 196 - log.Error(). 197 - Err(err). 198 - Str("method", "getRecord"). 199 - Str("collection", input.Collection). 200 - Str("rkey", input.RKey). 201 - Str("did", did.String()). 202 - Msg("PDS request failed") 151 + log.Error().Err(err).Str("method", "getRecord").Str("collection", input.Collection).Str("rkey", input.RKey).Str("did", did.String()).Msg("PDS request failed") 203 152 return nil, fmt.Errorf("failed to get record: %w", err) 204 153 } 205 154 206 - log.Debug(). 207 - Str("method", "getRecord"). 208 - Str("collection", input.Collection). 209 - Str("rkey", input.RKey). 210 - Str("did", did.String()). 211 - Str("uri", result.URI). 212 - Str("cid", result.CID). 213 - Msg("PDS request completed") 214 - 215 - return &GetRecordOutput{ 216 - URI: result.URI, 217 - CID: result.CID, 218 - Value: result.Value, 219 - }, nil 220 - } 221 - 222 - // ListRecordsInput contains parameters for listing records 223 - type ListRecordsInput struct { 224 - Collection string 225 - Limit *int64 226 - Cursor *string 227 - } 228 - 229 - // ListRecordsOutput contains the result of listing records 230 - type ListRecordsOutput struct { 231 - Records []Record 232 - Cursor *string 233 - } 234 - 235 - // Record represents a single record in a list 236 - type Record struct { 237 - URI string 238 - CID string 239 - Value map[string]any 155 + log.Debug().Str("method", "getRecord").Str("collection", input.Collection).Str("rkey", input.RKey).Str("did", did.String()).Str("uri", rec.URI).Str("cid", rec.CID).Msg("PDS request completed") 156 + return &GetRecordOutput{URI: rec.URI, CID: rec.CID, Value: rec.Value}, nil 240 157 } 241 158 242 - // ListRecords retrieves a list of records from a collection 243 159 func (c *Client) ListRecords(ctx context.Context, did syntax.DID, sessionID string, input *ListRecordsInput) (*ListRecordsOutput, error) { 244 160 ctx, span := tracing.PdsSpan(ctx, "listRecords", input.Collection, did.String()) 245 161 defer span.End() 246 162 247 - apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 163 + atpClient, err := c.getAtpClient(ctx, did, sessionID) 248 164 if err != nil { 249 165 tracing.EndWithError(span, err) 250 166 return nil, err 251 167 } 252 168 253 - // Build query parameters 254 - params := map[string]any{ 255 - "repo": did.String(), 256 - "collection": input.Collection, 257 - } 258 - 169 + var limit int 259 170 if input.Limit != nil { 260 - params["limit"] = *input.Limit 171 + limit = int(*input.Limit) 261 172 } 173 + var cursor string 262 174 if input.Cursor != nil { 263 - params["cursor"] = *input.Cursor 175 + cursor = *input.Cursor 264 176 } 265 177 266 - // Use the API client's Get method to call com.atproto.repo.listRecords 267 - var result struct { 268 - Records []struct { 269 - URI string `json:"uri"` 270 - CID string `json:"cid"` 271 - Value map[string]any `json:"value"` 272 - } `json:"records"` 273 - Cursor *string `json:"cursor,omitempty"` 274 - } 275 - 276 - err = apiClient.Get(ctx, "com.atproto.repo.listRecords", params, &result) 277 - recordCount := len(result.Records) 178 + result, err := atpClient.ListRecords(ctx, input.Collection, limit, cursor) 278 179 metrics.PDSRequestsTotal.WithLabelValues("listRecords", input.Collection).Inc() 279 180 280 181 if err != nil { 281 - err = wrapPDSError(err) 282 182 tracing.EndWithError(span, err) 283 - log.Error(). 284 - Err(err). 285 - Str("method", "listRecords"). 286 - Str("collection", input.Collection). 287 - Str("did", did.String()). 288 - Msg("PDS request failed") 183 + log.Error().Err(err).Str("method", "listRecords").Str("collection", input.Collection).Str("did", did.String()).Msg("PDS request failed") 289 184 return nil, fmt.Errorf("failed to list records: %w", err) 290 185 } 291 186 292 - logEvent := log.Debug(). 293 - Str("method", "listRecords"). 294 - Str("collection", input.Collection). 295 - Str("did", did.String()). 296 - Int("record_count", recordCount) 297 - 298 - if result.Cursor != nil && *result.Cursor != "" { 299 - logEvent.Str("cursor", *result.Cursor).Bool("has_more", true) 187 + logEvent := log.Debug().Str("method", "listRecords").Str("collection", input.Collection).Str("did", did.String()).Int("record_count", len(result.Records)) 188 + if result.Cursor != "" { 189 + logEvent.Str("cursor", result.Cursor).Bool("has_more", true) 300 190 } else { 301 191 logEvent.Bool("has_more", false) 302 192 } 303 - 304 193 logEvent.Msg("PDS request completed") 305 194 306 - // Convert to our output format 307 - records := make([]Record, len(result.Records)) 308 - for i, r := range result.Records { 309 - records[i] = Record{ 310 - URI: r.URI, 311 - CID: r.CID, 312 - Value: r.Value, 313 - } 195 + var cursorPtr *string 196 + if result.Cursor != "" { 197 + cursorPtr = &result.Cursor 314 198 } 315 199 316 200 return &ListRecordsOutput{ 317 - Records: records, 318 - Cursor: result.Cursor, 201 + Records: result.Records, 202 + Cursor: cursorPtr, 319 203 }, nil 320 204 } 321 205 322 - // ListAllRecords retrieves all records from a collection, handling pagination automatically 323 - // This is useful when you need to fetch the complete collection without worrying about pagination 324 206 func (c *Client) ListAllRecords(ctx context.Context, did syntax.DID, sessionID string, collection string) (*ListRecordsOutput, error) { 325 207 ctx, span := tracing.PdsSpan(ctx, "listAllRecords", collection, did.String()) 326 208 defer span.End() 327 209 328 - var allRecords []Record 329 - var cursor *string 330 - pageCount := 0 331 - 332 - // ATProto typically returns up to 100 records per page by default 333 - // We'll request 100 at a time and paginate through all results 334 - limit := int64(100) 335 - 336 - for { 337 - // Check for context cancellation before each page request 338 - // This allows long-running pagination to be cancelled gracefully 339 - select { 340 - case <-ctx.Done(): 341 - tracing.EndWithError(span, ctx.Err()) 342 - return nil, ctx.Err() 343 - default: 344 - } 345 - 346 - output, err := c.ListRecords(ctx, did, sessionID, &ListRecordsInput{ 347 - Collection: collection, 348 - Limit: &limit, 349 - Cursor: cursor, 350 - }) 351 - if err != nil { 352 - tracing.EndWithError(span, err) 353 - return nil, err 354 - } 355 - 356 - allRecords = append(allRecords, output.Records...) 357 - pageCount++ 358 - 359 - // If there's no cursor, we've fetched all records 360 - if output.Cursor == nil || *output.Cursor == "" { 361 - break 362 - } 210 + atpClient, err := c.getAtpClient(ctx, did, sessionID) 211 + if err != nil { 212 + tracing.EndWithError(span, err) 213 + return nil, err 214 + } 363 215 364 - cursor = output.Cursor 216 + records, err := atpClient.ListAllRecords(ctx, collection) 217 + if err != nil { 218 + tracing.EndWithError(span, err) 219 + return nil, err 365 220 } 366 221 367 - log.Info(). 368 - Str("method", "listAllRecords"). 369 - Str("collection", collection). 370 - Str("did", did.String()). 371 - Int("total_records", len(allRecords)). 372 - Int("pages_fetched", pageCount). 373 - Msg("PDS pagination completed") 222 + log.Info().Str("method", "listAllRecords").Str("collection", collection).Str("did", did.String()).Int("total_records", len(records)).Msg("PDS pagination completed") 374 223 375 224 return &ListRecordsOutput{ 376 - Records: allRecords, 377 - Cursor: nil, // All records fetched, no more pagination 225 + Records: records, 226 + Cursor: nil, 378 227 }, nil 379 228 } 380 229 381 - // PutRecordInput contains parameters for updating a record 382 - type PutRecordInput struct { 383 - Collection string 384 - RKey string 385 - Record any 386 - } 387 - 388 - // PutRecord updates an existing record in the user's repository 389 230 func (c *Client) PutRecord(ctx context.Context, did syntax.DID, sessionID string, input *PutRecordInput) error { 390 231 ctx, span := tracing.PdsSpan(ctx, "putRecord", input.Collection, did.String()) 391 232 defer span.End() 392 233 393 - apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 234 + atpClient, err := c.getAtpClient(ctx, did, sessionID) 394 235 if err != nil { 395 236 tracing.EndWithError(span, err) 396 237 return err 397 238 } 398 239 399 - // Build the request body 400 - body := map[string]any{ 401 - "repo": did.String(), 402 - "collection": input.Collection, 403 - "rkey": input.RKey, 404 - "record": input.Record, 405 - } 406 - 407 - // Use the API client's Post method to call com.atproto.repo.putRecord 408 - var result struct { 409 - URI string `json:"uri"` 410 - CID string `json:"cid"` 411 - } 412 - 413 - err = apiClient.Post(ctx, "com.atproto.repo.putRecord", body, &result) 240 + _, _, err = atpClient.PutRecord(ctx, input.Collection, input.RKey, input.Record) 414 241 metrics.PDSRequestsTotal.WithLabelValues("putRecord", input.Collection).Inc() 415 242 416 243 if err != nil { 417 - err = wrapPDSError(err) 418 244 tracing.EndWithError(span, err) 419 - log.Error(). 420 - Err(err). 421 - Str("method", "putRecord"). 422 - Str("collection", input.Collection). 423 - Str("rkey", input.RKey). 424 - Str("did", did.String()). 425 - Msg("PDS request failed") 245 + log.Error().Err(err).Str("method", "putRecord").Str("collection", input.Collection).Str("rkey", input.RKey).Str("did", did.String()).Msg("PDS request failed") 426 246 return fmt.Errorf("failed to update record: %w", err) 427 247 } 428 248 429 - log.Debug(). 430 - Str("method", "putRecord"). 431 - Str("collection", input.Collection). 432 - Str("rkey", input.RKey). 433 - Str("did", did.String()). 434 - Str("uri", result.URI). 435 - Str("cid", result.CID). 436 - Msg("PDS request completed") 437 - 249 + log.Debug().Str("method", "putRecord").Str("collection", input.Collection).Str("rkey", input.RKey).Str("did", did.String()).Msg("PDS request completed") 438 250 return nil 439 251 } 440 252 441 - // DeleteRecordInput contains parameters for deleting a record 442 - type DeleteRecordInput struct { 443 - Collection string 444 - RKey string 445 - } 446 - 447 - // DeleteRecord deletes a record from the user's repository 448 253 func (c *Client) DeleteRecord(ctx context.Context, did syntax.DID, sessionID string, input *DeleteRecordInput) error { 449 254 ctx, span := tracing.PdsSpan(ctx, "deleteRecord", input.Collection, did.String()) 450 255 defer span.End() 451 256 452 - apiClient, err := c.getAuthenticatedAPIClient(ctx, did, sessionID) 257 + atpClient, err := c.getAtpClient(ctx, did, sessionID) 453 258 if err != nil { 454 259 tracing.EndWithError(span, err) 455 260 return err 456 261 } 457 262 458 - // Build the request body 459 - body := map[string]any{ 460 - "repo": did.String(), 461 - "collection": input.Collection, 462 - "rkey": input.RKey, 463 - } 464 - 465 - // Use the API client's Post method to call com.atproto.repo.deleteRecord 466 - var result struct{} 467 - 468 - err = apiClient.Post(ctx, "com.atproto.repo.deleteRecord", body, &result) 263 + err = atpClient.DeleteRecord(ctx, input.Collection, input.RKey) 469 264 metrics.PDSRequestsTotal.WithLabelValues("deleteRecord", input.Collection).Inc() 470 265 471 266 if err != nil { 472 - err = wrapPDSError(err) 473 267 tracing.EndWithError(span, err) 474 - log.Error(). 475 - Err(err). 476 - Str("method", "deleteRecord"). 477 - Str("collection", input.Collection). 478 - Str("rkey", input.RKey). 479 - Str("did", did.String()). 480 - Msg("PDS request failed") 268 + log.Error().Err(err).Str("method", "deleteRecord").Str("collection", input.Collection).Str("rkey", input.RKey).Str("did", did.String()).Msg("PDS request failed") 481 269 return fmt.Errorf("failed to delete record: %w", err) 482 270 } 483 271 484 - log.Debug(). 485 - Str("method", "deleteRecord"). 486 - Str("collection", input.Collection). 487 - Str("rkey", input.RKey). 488 - Str("did", did.String()). 489 - Msg("PDS request completed") 490 - 272 + log.Debug().Str("method", "deleteRecord").Str("collection", input.Collection).Str("rkey", input.RKey).Str("did", did.String()).Msg("PDS request completed") 491 273 return nil 492 274 }
+7 -14
internal/atproto/nsid.go
··· 1 1 package atproto 2 2 3 3 import ( 4 - "fmt" 5 4 "regexp" 5 + 6 + "tangled.org/pdewey.com/atp" 6 7 ) 7 8 8 9 // NSID (Namespaced Identifier) constants for Arabica lexicons. ··· 45 46 return rkeyRegex.MatchString(rkey) 46 47 } 47 48 48 - // BuildATURI constructs an AT-URI from a DID, collection NSID, and record key 49 - func BuildATURI(did, collection, rkey string) string { 50 - return fmt.Sprintf("at://%s/%s/%s", did, collection, rkey) 51 - } 49 + // BuildATURI constructs an AT-URI from a DID, collection NSID, and record key. 50 + var BuildATURI = atp.BuildATURI 52 51 53 - // ExtractRKeyFromURI extracts the record key from an AT-URI 54 - // Returns the rkey if successful, empty string if parsing fails 55 - func ExtractRKeyFromURI(uri string) string { 56 - components, err := ResolveATURI(uri) 57 - if err != nil { 58 - return "" 59 - } 60 - return components.RKey 61 - } 52 + // ExtractRKeyFromURI extracts the record key from an AT-URI. 53 + // Returns the rkey if successful, empty string if parsing fails. 54 + var ExtractRKeyFromURI = atp.RKeyFromURI
+49 -316
internal/atproto/public_client.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "encoding/json" 6 - "errors" 7 - "fmt" 8 - "net" 9 5 "net/http" 10 - "net/url" 11 - "slices" 12 - "strings" 13 - "sync" 14 6 "time" 15 7 16 - "arabica/internal/tracing" 8 + "tangled.org/pdewey.com/atp" 17 9 18 10 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 19 11 ) 20 12 21 - const ( 22 - // PublicAPIBaseURL is the public Bluesky API endpoint 23 - PublicAPIBaseURL = "https://public.api.bsky.app" 24 - // PLCDirectoryURL is the PLC directory for resolving DIDs 25 - PLCDirectoryURL = "https://plc.directory" 26 - ) 27 - 28 - // ErrSSRFBlocked is returned when a potential SSRF attack is blocked 29 - var ErrSSRFBlocked = errors.New("request blocked: potential SSRF detected") 30 - 31 - // isPrivateIP checks if an IP address is in a private/internal range 32 - func isPrivateIP(ip net.IP) bool { 33 - if ip == nil { 34 - return false 35 - } 36 - 37 - // Check for loopback 38 - if ip.IsLoopback() { 39 - return true 40 - } 41 - 42 - // Check for link-local 43 - if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { 44 - return true 45 - } 46 - 47 - // Check for private ranges 48 - if ip.IsPrivate() { 49 - return true 50 - } 51 - 52 - // Check for unspecified (0.0.0.0) 53 - if ip.IsUnspecified() { 54 - return true 55 - } 56 - 57 - // Check for cloud metadata endpoint (169.254.169.254) 58 - if ip.Equal(net.ParseIP("169.254.169.254")) { 59 - return true 60 - } 61 - 62 - return false 63 - } 64 - 65 - // validateDomain checks if a domain is safe to connect to (not internal/private) 66 - func validateDomain(domain string) error { 67 - // Block obviously dangerous patterns 68 - if domain == "localhost" || strings.HasSuffix(domain, ".local") { 69 - return ErrSSRFBlocked 70 - } 71 - 72 - // Check for IP addresses embedded in the domain 73 - if ip := net.ParseIP(domain); ip != nil { 74 - if isPrivateIP(ip) { 75 - return ErrSSRFBlocked 76 - } 77 - } 78 - 79 - // Resolve the domain and check all IPs 80 - ips, err := net.LookupIP(domain) 81 - if err != nil { 82 - // If we can't resolve it, let the HTTP request fail later 83 - return nil 84 - } 13 + // Profile is a user's public profile. It is a type alias for atp.PublicProfile 14 + // so existing callers continue to work without changes. 15 + type Profile = atp.PublicProfile 85 16 86 - if slices.ContainsFunc(ips, isPrivateIP) { 87 - return ErrSSRFBlocked 88 - } 89 - 90 - return nil 91 - } 92 - 93 - // PublicClient provides unauthenticated access to public ATProto APIs 17 + // PublicClient wraps atp.PublicClient and exposes the same method signatures 18 + // that arabica callers already use (GetRecord, ListRecords, etc.). 94 19 type PublicClient struct { 95 - baseURL string 96 - httpClient *http.Client 97 - // Cache PDS endpoints to avoid repeated lookups 98 - pdsCache map[string]string 99 - pdsCacheMu sync.RWMutex 20 + inner *atp.PublicClient 100 21 } 101 22 102 - // NewPublicClient creates a new public API client 23 + // NewPublicClient creates a PublicClient with OTel-instrumented HTTP transport. 103 24 func NewPublicClient() *PublicClient { 104 - return &PublicClient{ 105 - baseURL: PublicAPIBaseURL, 106 - httpClient: &http.Client{ 107 - Timeout: 30 * time.Second, 108 - Transport: otelhttp.NewTransport(http.DefaultTransport), 109 - }, 110 - pdsCache: make(map[string]string), 25 + hc := &http.Client{ 26 + Timeout: 30 * time.Second, 27 + Transport: otelhttp.NewTransport(http.DefaultTransport), 111 28 } 29 + return &PublicClient{inner: atp.NewPublicClientWithHTTP(hc)} 112 30 } 113 31 114 - // GetPDSEndpoint resolves a DID to find the user's PDS endpoint 32 + // GetPDSEndpoint resolves a DID to the user's PDS base URL. 115 33 func (c *PublicClient) GetPDSEndpoint(ctx context.Context, did string) (string, error) { 116 - // Check cache first 117 - c.pdsCacheMu.RLock() 118 - if pds, ok := c.pdsCache[did]; ok { 119 - c.pdsCacheMu.RUnlock() 120 - return pds, nil 121 - } 122 - c.pdsCacheMu.RUnlock() 123 - 124 - ctx, span := tracing.PdsSpan(ctx, "resolvePDS", "did:plc", did) 125 - defer span.End() 126 - 127 - // Resolve DID document from PLC directory 128 - var pdsEndpoint string 34 + return c.inner.GetPDSEndpoint(ctx, did) 35 + } 129 36 130 - if strings.HasPrefix(did, "did:plc:") { 131 - // PLC DID - resolve from plc.directory 132 - reqURL := fmt.Sprintf("%s/%s", PLCDirectoryURL, did) 133 - req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) 134 - if err != nil { 135 - return "", fmt.Errorf("creating request: %w", err) 136 - } 137 - 138 - resp, err := c.httpClient.Do(req) 139 - if err != nil { 140 - return "", fmt.Errorf("fetching DID document: %w", err) 141 - } 142 - defer resp.Body.Close() 143 - 144 - if resp.StatusCode != http.StatusOK { 145 - return "", fmt.Errorf("DID resolution failed with status %d", resp.StatusCode) 146 - } 147 - 148 - var didDoc struct { 149 - Service []struct { 150 - ID string `json:"id"` 151 - Type string `json:"type"` 152 - ServiceEndpoint string `json:"serviceEndpoint"` 153 - } `json:"service"` 154 - } 155 - if err := json.NewDecoder(resp.Body).Decode(&didDoc); err != nil { 156 - return "", fmt.Errorf("decoding DID document: %w", err) 157 - } 158 - 159 - // Find the atproto_pds service 160 - for _, svc := range didDoc.Service { 161 - if svc.ID == "#atproto_pds" || svc.Type == "AtprotoPersonalDataServer" { 162 - pdsEndpoint = svc.ServiceEndpoint 163 - break 164 - } 165 - } 166 - } else if after, ok := strings.CutPrefix(did, "did:web:"); ok { 167 - // Web DID - the domain is the PDS 168 - // Validate domain to prevent SSRF attacks 169 - domain := after 170 - // Handle percent-encoded colons for ports (e.g., did:web:example.com%3A8080) 171 - domain = strings.ReplaceAll(domain, "%3A", ":") 172 - 173 - // Extract just the host part (without path) 174 - if idx := strings.Index(domain, "/"); idx != -1 { 175 - domain = domain[:idx] 176 - } 177 - 178 - // Validate the domain is safe 179 - host := domain 180 - if hostPart, _, err := net.SplitHostPort(domain); err == nil { 181 - host = hostPart 182 - } 183 - if err := validateDomain(host); err != nil { 184 - return "", err 185 - } 186 - 187 - pdsEndpoint = "https://" + domain 188 - } 189 - 190 - if pdsEndpoint == "" { 191 - return "", fmt.Errorf("could not resolve PDS endpoint for %s", did) 192 - } 193 - 194 - // Cache the result 195 - c.pdsCacheMu.Lock() 196 - c.pdsCache[did] = pdsEndpoint 197 - c.pdsCacheMu.Unlock() 198 - 199 - return pdsEndpoint, nil 37 + // GetProfile fetches a user's public profile by DID or handle. 38 + func (c *PublicClient) GetProfile(ctx context.Context, actor string) (*Profile, error) { 39 + return c.inner.GetProfile(ctx, actor) 200 40 } 201 41 202 - // Profile represents a user's public profile 203 - type Profile struct { 204 - DID string `json:"did"` 205 - Handle string `json:"handle"` 206 - DisplayName *string `json:"displayName,omitempty"` 207 - Avatar *string `json:"avatar,omitempty"` 42 + // ResolveHandle resolves an AT Protocol handle to a DID. 43 + func (c *PublicClient) ResolveHandle(ctx context.Context, handle string) (string, error) { 44 + return c.inner.ResolveHandle(ctx, handle) 208 45 } 209 46 210 - // PublicListRecordsOutput represents the response from public listRecords API 47 + // PublicListRecordsOutput represents the response from public listRecords API. 211 48 type PublicListRecordsOutput struct { 212 49 Records []PublicRecordEntry `json:"records"` 213 50 Cursor *string `json:"cursor,omitempty"` 214 51 } 215 52 216 - // PublicRecordEntry represents a single record in the public listRecords response 53 + // PublicRecordEntry represents a single record in the public listRecords response. 217 54 type PublicRecordEntry struct { 218 55 URI string `json:"uri"` 219 56 CID string `json:"cid"` 220 57 Value map[string]any `json:"value"` 221 58 } 222 59 223 - // GetProfile fetches a user's public profile by DID or handle 224 - func (c *PublicClient) GetProfile(ctx context.Context, actor string) (*Profile, error) { 225 - ctx, span := tracing.PdsSpan(ctx, "getProfile", "app.bsky.actor", actor) 226 - defer span.End() 227 - 228 - reqURL := fmt.Sprintf("%s/xrpc/app.bsky.actor.getProfile?actor=%s", 229 - c.baseURL, url.QueryEscape(actor)) 230 - 231 - req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) 232 - if err != nil { 233 - return nil, fmt.Errorf("creating request: %w", err) 234 - } 235 - 236 - resp, err := c.httpClient.Do(req) 237 - if err != nil { 238 - return nil, fmt.Errorf("fetching profile: %w", err) 239 - } 240 - defer resp.Body.Close() 241 - 242 - if resp.StatusCode != http.StatusOK { 243 - return nil, fmt.Errorf("profile request failed with status %d", resp.StatusCode) 244 - } 245 - 246 - var profile Profile 247 - if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil { 248 - return nil, fmt.Errorf("decoding profile: %w", err) 249 - } 250 - 251 - return &profile, nil 252 - } 253 - 254 - // ListRecords fetches public records from a user's repository 255 - // Records are returned in reverse chronological order (newest first) 256 - // This queries the user's PDS directly to support custom collections 60 + // ListRecords fetches public records from a user's repository via their PDS, 61 + // newest-first. 257 62 func (c *PublicClient) ListRecords(ctx context.Context, did, collection string, limit int) (*PublicListRecordsOutput, error) { 258 - ctx, span := tracing.PdsSpan(ctx, "publicListRecords", collection, did) 259 - defer span.End() 260 - 261 - // Resolve the user's PDS endpoint 262 - pdsEndpoint, err := c.GetPDSEndpoint(ctx, did) 263 - if err != nil { 264 - return nil, fmt.Errorf("resolving PDS: %w", err) 265 - } 266 - 267 - reqURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s&limit=%d&reverse=true", 268 - pdsEndpoint, url.QueryEscape(did), url.QueryEscape(collection), limit) 269 - 270 - req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) 271 - if err != nil { 272 - return nil, fmt.Errorf("creating request: %w", err) 273 - } 274 - 275 - resp, err := c.httpClient.Do(req) 276 - if err != nil { 277 - return nil, fmt.Errorf("listing records: %w", err) 278 - } 279 - defer resp.Body.Close() 280 - 281 - if resp.StatusCode != http.StatusOK { 282 - return nil, fmt.Errorf("list records request failed with status %d", resp.StatusCode) 283 - } 284 - 285 - var output PublicListRecordsOutput 286 - if err := json.NewDecoder(resp.Body).Decode(&output); err != nil { 287 - return nil, fmt.Errorf("decoding records: %w", err) 288 - } 289 - 290 - return &output, nil 291 - } 292 - 293 - // ResolveHandle resolves an AT Protocol handle to a DID 294 - func (c *PublicClient) ResolveHandle(ctx context.Context, handle string) (string, error) { 295 - ctx, span := tracing.PdsSpan(ctx, "resolveHandle", "com.atproto.identity", handle) 296 - defer span.End() 297 - 298 - reqURL := fmt.Sprintf("%s/xrpc/com.atproto.identity.resolveHandle?handle=%s", 299 - c.baseURL, url.QueryEscape(handle)) 300 - 301 - req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) 302 - if err != nil { 303 - return "", fmt.Errorf("creating request: %w", err) 304 - } 305 - 306 - resp, err := c.httpClient.Do(req) 63 + records, cursor, err := c.inner.ListPublicRecords(ctx, did, collection, atp.ListPublicRecordsOpts{ 64 + Limit: limit, 65 + Reverse: true, 66 + }) 307 67 if err != nil { 308 - return "", fmt.Errorf("resolving handle: %w", err) 68 + return nil, err 309 69 } 310 - defer resp.Body.Close() 311 70 312 - if resp.StatusCode == http.StatusNotFound { 313 - return "", fmt.Errorf("handle not found: %s", handle) 71 + out := &PublicListRecordsOutput{ 72 + Records: make([]PublicRecordEntry, len(records)), 314 73 } 315 - 316 - if resp.StatusCode != http.StatusOK { 317 - return "", fmt.Errorf("resolve handle failed with status %d", resp.StatusCode) 74 + for i, r := range records { 75 + out.Records[i] = PublicRecordEntry{ 76 + URI: r.URI, 77 + CID: r.CID, 78 + Value: r.Value, 79 + } 318 80 } 319 - 320 - var result struct { 321 - DID string `json:"did"` 81 + if cursor != "" { 82 + out.Cursor = &cursor 322 83 } 323 - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 324 - return "", fmt.Errorf("decoding response: %w", err) 325 - } 326 - 327 - return result.DID, nil 84 + return out, nil 328 85 } 329 86 330 - // GetRecord fetches a single public record from the user's PDS 87 + // GetRecord fetches a single public record from a user's PDS. 331 88 func (c *PublicClient) GetRecord(ctx context.Context, did, collection, rkey string) (*PublicRecordEntry, error) { 332 - ctx, span := tracing.PdsSpan(ctx, "publicGetRecord", collection, did) 333 - defer span.End() 334 - 335 - // Resolve the user's PDS endpoint 336 - pdsEndpoint, err := c.GetPDSEndpoint(ctx, did) 89 + r, err := c.inner.GetPublicRecord(ctx, did, collection, rkey) 337 90 if err != nil { 338 - return nil, fmt.Errorf("resolving PDS: %w", err) 91 + return nil, err 339 92 } 340 - 341 - reqURL := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=%s&rkey=%s", 342 - pdsEndpoint, url.QueryEscape(did), url.QueryEscape(collection), url.QueryEscape(rkey)) 343 - 344 - req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) 345 - if err != nil { 346 - return nil, fmt.Errorf("creating request: %w", err) 347 - } 348 - 349 - resp, err := c.httpClient.Do(req) 350 - if err != nil { 351 - return nil, fmt.Errorf("getting record: %w", err) 352 - } 353 - defer resp.Body.Close() 354 - 355 - if resp.StatusCode != http.StatusOK { 356 - return nil, fmt.Errorf("get record request failed with status %d", resp.StatusCode) 357 - } 358 - 359 - var entry PublicRecordEntry 360 - if err := json.NewDecoder(resp.Body).Decode(&entry); err != nil { 361 - return nil, fmt.Errorf("decoding record: %w", err) 362 - } 363 - 364 - return &entry, nil 93 + return &PublicRecordEntry{ 94 + URI: r.URI, 95 + CID: r.CID, 96 + Value: r.Value, 97 + }, nil 365 98 }
+7 -6
internal/atproto/resolver.go
··· 7 7 "arabica/internal/models" 8 8 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 + "tangled.org/pdewey.com/atp" 10 11 ) 11 12 12 13 // ATURIComponents holds the parsed components of an AT-URI ··· 16 17 RKey string 17 18 } 18 19 19 - // ResolveATURI parses an AT-URI and returns its components 20 + // ResolveATURI parses an AT-URI and returns its components. 20 21 // AT-URI format: at://did:plc:abc123/social.arabica.brew/3jxyabc 21 22 func ResolveATURI(uri string) (*ATURIComponents, error) { 22 - atURI, err := syntax.ParseATURI(uri) 23 + did, collection, rkey, err := atp.ParseATURI(uri) 23 24 if err != nil { 24 - return nil, fmt.Errorf("invalid AT-URI: %w", err) 25 + return nil, err 25 26 } 26 27 27 28 return &ATURIComponents{ 28 - DID: atURI.Authority().String(), 29 - Collection: atURI.Collection().String(), 30 - RKey: atURI.RecordKey().String(), 29 + DID: did, 30 + Collection: collection, 31 + RKey: rkey, 31 32 }, nil 32 33 } 33 34
+16 -82
internal/tracing/tracing.go
··· 1 + // Package tracing re-exports atp/tracing span helpers and provides 2 + // arabica-specific initialization (zerolog bridge). 1 3 package tracing 2 4 3 5 import ( 4 6 "context" 5 - "os" 6 7 7 8 "github.com/go-logr/zerologr" 8 9 "github.com/rs/zerolog/log" 9 10 "go.opentelemetry.io/otel" 10 11 "go.opentelemetry.io/otel/attribute" 11 - "go.opentelemetry.io/otel/codes" 12 - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 13 - "go.opentelemetry.io/otel/propagation" 14 - "go.opentelemetry.io/otel/sdk/resource" 15 12 sdktrace "go.opentelemetry.io/otel/sdk/trace" 16 - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" 17 13 "go.opentelemetry.io/otel/trace" 14 + 15 + atptracing "tangled.org/pdewey.com/atp/tracing" 18 16 ) 19 17 20 - // tracer returns the package tracer. This must be a function (not a package-level var) 21 - // because the global TracerProvider isn't set until Init() runs. 22 - func tracer() trace.Tracer { 23 - return otel.Tracer("arabica") 24 - } 25 - 26 18 // Init creates and registers a tracer provider with an OTLP HTTP exporter. 27 - // It reads OTEL_EXPORTER_OTLP_ENDPOINT (default: localhost:4318). 28 - // Returns the provider so the caller can defer Shutdown. 19 + // Bridges OTel's internal logger to zerolog before delegating to atp/tracing. 29 20 func Init(ctx context.Context) (*sdktrace.TracerProvider, error) { 30 - // Bridge OTel's internal logger to zerolog 31 21 otel.SetLogger(zerologr.New(&log.Logger)) 32 - 33 - endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") 34 - if endpoint == "" { 35 - endpoint = "localhost:4318" 36 - } 37 - 38 - exp, err := otlptracehttp.New(ctx, 39 - otlptracehttp.WithEndpoint(endpoint), 40 - otlptracehttp.WithInsecure(), 41 - ) 42 - if err != nil { 43 - return nil, err 44 - } 45 - 46 - tp := sdktrace.NewTracerProvider( 47 - sdktrace.WithBatcher(exp), 48 - sdktrace.WithResource(resource.NewWithAttributes( 49 - semconv.SchemaURL, 50 - semconv.ServiceNameKey.String("arabica"), 51 - )), 52 - ) 53 - 54 - otel.SetTracerProvider(tp) 55 - otel.SetTextMapPropagator(propagation.TraceContext{}) 56 - 57 - return tp, nil 22 + return atptracing.Init(ctx, "arabica") 58 23 } 59 24 60 - // BoltSpan starts a span for a BoltDB operation with standard attributes. 61 - // Returns a no-op span if there is no parent span in ctx (e.g. background goroutines). 25 + // BoltSpan starts a span for a BoltDB operation. 62 26 func BoltSpan(ctx context.Context, op, bucket string) (context.Context, trace.Span) { 63 - if !trace.SpanFromContext(ctx).SpanContext().IsValid() { 64 - return ctx, trace.SpanFromContext(ctx) 65 - } 66 - return tracer().Start(ctx, "bolt."+op, 67 - trace.WithAttributes( 68 - attribute.String("db.system", "boltdb"), 69 - attribute.String("db.operation", op), 70 - attribute.String("bolt.bucket", bucket), 71 - ), 72 - ) 27 + return atptracing.BoltSpan(ctx, op, bucket) 73 28 } 74 29 75 - // PdsSpan starts a span for a PDS operation with standard attributes. 76 - func PdsSpan(ctx context.Context, method, collection, did string) (context.Context, trace.Span) { 77 - return tracer().Start(ctx, "pds."+method, 78 - trace.WithAttributes( 79 - attribute.String("pds.method", method), 80 - attribute.String("pds.collection", collection), 81 - attribute.String("pds.did", did), 82 - ), 83 - ) 30 + // SqliteSpan starts a span for a SQLite operation. 31 + func SqliteSpan(ctx context.Context, op, table string) (context.Context, trace.Span) { 32 + return atptracing.SqliteSpan(ctx, op, table) 84 33 } 85 34 86 - // SqliteSpan starts a span for a SQLite operation with standard attributes. 87 - // Returns a no-op span if there is no parent span in ctx. 88 - func SqliteSpan(ctx context.Context, op, table string) (context.Context, trace.Span) { 89 - if !trace.SpanFromContext(ctx).SpanContext().IsValid() { 90 - return ctx, trace.SpanFromContext(ctx) 91 - } 92 - return tracer().Start(ctx, "sqlite."+op, 93 - trace.WithAttributes( 94 - attribute.String("db.system", "sqlite"), 95 - attribute.String("db.operation", op), 96 - attribute.String("db.sql.table", table), 97 - ), 98 - ) 35 + // PdsSpan starts a span for a PDS XRPC operation. 36 + func PdsSpan(ctx context.Context, method, collection, did string) (context.Context, trace.Span) { 37 + return atptracing.PdsSpan(ctx, method, collection, did) 99 38 } 100 39 101 40 // HandlerSpan starts a span for a logical operation within a handler. 102 - // Use this to group related work (e.g. a refresh loop) under a single span. 103 41 func HandlerSpan(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span) { 104 - return tracer().Start(ctx, name, trace.WithAttributes(attrs...)) 42 + return atptracing.HandlerSpan(ctx, name, attrs...) 105 43 } 106 44 107 45 // EndWithError records an error on a span and sets its status. 108 - // If err is nil, this is a no-op. 109 46 func EndWithError(span trace.Span, err error) { 110 - if err != nil { 111 - span.RecordError(err) 112 - span.SetStatus(codes.Error, err.Error()) 113 - } 47 + atptracing.EndWithError(span, err) 114 48 }
+1 -1
nix/default.nix
··· 4 4 pname = "arabica"; 5 5 version = "0.1.0"; 6 6 src = ../.; 7 - vendorHash = "sha256-s4z7nsPP9H5Gm25osh1vmI1Uta/LmjDb0RKhpKTNYlo="; 7 + vendorHash = "sha256-WyxF5rkiA8vMu0wwbnLyamfKY/+Axi7KXV5TSz6ii2c="; 8 8 9 9 nativeBuildInputs = [ templ tailwindcss ]; 10 10