backend for xcvr appview
3
fork

Configure Feed

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

beep

rachel-mp4 dda1e1d3 9a07192a

+1399 -196
+2
.gitignore
··· 23 23 24 24 # env file 25 25 .env 26 + 27 + jwks.json
+41
lexicons/org/xcvr/actor/getChannel.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "org.xcvr.actor.resolveChannel", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "get the url of a channel", 8 + "parameters": { 9 + "type": "params" 10 + "union": [ 11 + { 12 + "type": "object", 13 + "required": ["handle", "rkey"], 14 + "properties": { 15 + "handle": {"type": "string"}, 16 + "rkey": {"type": "string"} 17 + } 18 + }, 19 + { 20 + "type": "object", 21 + "required": ["did", "rkey"], 22 + "properties": { 23 + "did": {"type": "string"}, 24 + "rkey": {"type": "string"} 25 + } 26 + } 27 + ] 28 + }, 29 + "output": { 30 + "encoding": "application/json", 31 + "schema": { 32 + "type": "object", 33 + "required": ["url"], 34 + "properties": { 35 + "url": {"type": "string"} 36 + } 37 + } 38 + } 39 + } 40 + } 41 + }
+41
lexicons/org/xcvr/actor/resolveChannel.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "org.xcvr.actor.resolveChannel", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "get the url of a channel", 8 + "parameters": { 9 + "type": "params" 10 + "union": [ 11 + { 12 + "type": "object", 13 + "required": ["handle", "rkey"], 14 + "properties": { 15 + "handle": {"type": "string"}, 16 + "rkey": {"type": "string"} 17 + } 18 + }, 19 + { 20 + "type": "object", 21 + "required": ["did", "rkey"], 22 + "properties": { 23 + "did": {"type": "string"}, 24 + "rkey": {"type": "string"} 25 + } 26 + } 27 + ] 28 + }, 29 + "output": { 30 + "encoding": "application/json", 31 + "schema": { 32 + "type": "object", 33 + "required": ["url"], 34 + "properties": { 35 + "url": {"type": "string"} 36 + } 37 + } 38 + } 39 + } 40 + } 41 + }
+1 -1
lexicons/org/xcvr/feed/channel.json
··· 30 30 } 31 31 } 32 32 } 33 - } 33 + }
+4 -2
migrations/001_init.down.sql
··· 5 5 DROP TABLE signets; 6 6 7 7 DROP TABLE channels; 8 - DROP TABLE did_handle; 9 - DROP TABLE profiles; 8 + DROP TABLE did_handles; 9 + DROP TABLE profiles; 10 + 11 + DROP TABLE
+29
migrations/001_init.up.sql
··· 55 55 ); 56 56 57 57 CREATE INDEX ON messages (signet_uri); 58 + 59 + CREATE TABLE oauthrequests ( 60 + id SERIAL PRIMARY KEY, 61 + authserver_iss TEXT, 62 + state TEXT, 63 + did TEXT, 64 + pds_url TEXT, 65 + pkce_verifier TEXT, 66 + dpop_auth_server_nonce TEXT, 67 + dpop_private_jwk TEXT, 68 + UNIQUE(did, state) 69 + ); 70 + 71 + 72 + CREATE TABLE oauthsessions ( 73 + id SERIAL PRIMARY KEY, 74 + authserver_iss TEXT, 75 + state TEXT, 76 + did TEXT, 77 + pds_url TEXT, 78 + pkce_verifier TEXT, 79 + dpop_auth_server_nonce TEXT, 80 + dpop_private_jwk TEXT, 81 + dpop_pds_nonce TEXT, 82 + access_token TEXT, 83 + refresh_token TEXT, 84 + expiration TIMESTAMPTZ 85 + UNIQUE(did, state) 86 + );
+1 -1
migrations/002_populate.down.sql
··· 2 2 DELETE FROM signets; 3 3 DELETE FROM channels; 4 4 DELETE FROM did_handles; 5 - DELETE FROM profiles; 5 + DELETE FROM profiles
+6 -6
migrations/002_populate.up.sql
··· 10 10 11 11 INSERT INTO channels (uri, cid, did, host, title, topic, created_at) 12 12 VALUES 13 - ('at://did:example:alice/org.xcvr.channel/general', 'chanCid1', 'did:example:alice', 'alice.com', 'General Chat', 'All-purpose chatter', now() - interval '2 days'), 14 - ('at://did:example:bob/org.xcvr.channel/help', 'chanCid2', 'did:example:bob', 'bob.net', 'Help Channel', 'Support and help', now() - interval '1 day'); 13 + ('at://did:example:alice/org.xcvr.feed.channel/general', 'chanCid1', 'did:example:alice', 'xcvr.org', 'General Chat', 'All-purpose chatter', now() - interval '2 days'), 14 + ('at://did:example:bob/org.xcvr.feed.channel/help', 'chanCid2', 'did:example:bob', 'xcvr.org', 'Help Channel', 'Support and help', now() - interval '1 day'); 15 15 16 16 INSERT INTO signets (uri, did, channel_uri, message_id, cid) 17 17 VALUES 18 - ('at://did:example:alice/org.xcvr.lrc/signet1', 'did:example:alice', 'at://did:example:alice/org.xcvr.channel/general', 1, 'signetCid1'), 19 - ('at://did:example:bob/org.xcvr.lrc/signet2', 'did:example:bob', 'at://did:example:bob/org.xcvr.channel/help', 2, 'signetCid2'); 18 + ('at://did:example:xcvr/org.xcvr.lrc.signet/signet1', 'did:example:alice', 'at://did:example:alice/org.xcvr.feed.channel/general', 1, 'signetCid1'), 19 + ('at://did:example:xcvr/org.xcvr.lrc.signet/signet2', 'did:example:bob', 'at://did:example:bob/org.xcvr.feed.channel/help', 2, 'signetCid2'); 20 20 21 21 INSERT INTO messages (uri, did, signet_uri, body, nick, color, cid) 22 22 VALUES 23 - ('at://did:example:alice/org.xcvr.lrc.message/msg1', 'did:example:alice', 'at://did:example:alice/org.xcvr.lrc/signet1', 'Hey, welcome to the general chat!', 'alice', 16711680, 'msgCid1'), 24 - ('at://did:example:bob/org.xcvr.lrc.message/msg2', 'did:example:bob', 'at://did:example:bob/org.xcvr.lrc/signet2', 'How can I help you today?', 'bobby', 65280, 'msgCid2'); 23 + ('at://did:example:alice/org.xcvr.lrc.message/msg1', 'did:example:alice', 'at://did:example:xcvr/org.xcvr.lrc.signet/signet1', 'Hey, welcome to the general chat!', 'alice', 16711680, 'msgCid1'), 24 + ('at://did:example:bob/org.xcvr.lrc.message/msg2', 'did:example:bob', 'at://did:example:xcvr/org.xcvr.lrc.signet/signet2', 'How can I help you today?', 'bobby', 65280, 'msgCid2');
+18 -4
server/cmd/main.go
··· 3 3 import ( 4 4 "net/http" 5 5 "os" 6 - "context" 6 + "time" 7 7 "xcvr-backend/internal/db" 8 8 "xcvr-backend/internal/handler" 9 9 "xcvr-backend/internal/log" 10 + "xcvr-backend/internal/oauth" 11 + "xcvr-backend/internal/model" 10 12 11 13 "github.com/joho/godotenv" 12 14 ) ··· 20 22 logger.Println("i think you should make a .env file in the xcvr-backend directory !\n\nExample contents:\n-------------------------------------------------------------------\nPOSTGRES_USER=xcvr\nPOSTGRES_PASSWORD=secret\nPOSTGRES_DB=xcvrdb\nPOSTGRES_PORT=15432\n-------------------------------------------------------------------\n\nGood luck !\n\n") 21 23 panic(gdeerr) 22 24 } 23 - conn, err := db.Init() 24 - defer conn.Close(context.Background()) 25 + store, err := db.Init() 26 + defer store.Close() 25 27 if err != nil { 26 28 logger.Println("failed to init db") 27 29 panic(err) 28 30 } 29 - h := handler.New(conn, logger) 31 + model.Init(store) 32 + httpclient := &http.Client{ 33 + Timeout: 5 * time.Second, 34 + Transport: &http.Transport{ 35 + IdleConnTimeout: 90 * time.Second, 36 + }, 37 + } 38 + oauthclient, err := oauth.NewService(httpclient) 39 + if err != nil { 40 + logger.Println(err.Error()) 41 + panic(err) 42 + } 43 + h := handler.New(store, logger, oauthclient) 30 44 http.ListenAndServe(":8080", h.WithCORSAll()) 31 45 32 46 }
+69 -9
server/go.mod
··· 1 1 module xcvr-backend 2 2 3 - go 1.22.1 3 + go 1.24.2 4 4 5 - require github.com/rachel-mp4/lrc/lrcd v0.0.0-20250410194244-d1bffda40b72 5 + require ( 6 + github.com/gorilla/sessions v1.4.0 7 + github.com/haileyok/atproto-oauth-golang v0.0.2 8 + github.com/jackc/pgx/v5 v5.7.4 9 + github.com/joho/godotenv v1.5.1 10 + github.com/lestrrat-go/jwx/v2 v2.0.12 11 + github.com/rachel-mp4/lrcd v0.0.0-20250603192958-089ba44e79a5 12 + ) 6 13 7 14 require ( 15 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 // indirect 16 + github.com/carlmjohnson/versioninfo v0.22.5 // indirect 17 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 18 + github.com/felixge/httpsnoop v1.0.4 // indirect 19 + github.com/go-logr/logr v1.4.2 // indirect 20 + github.com/go-logr/stdr v1.2.2 // indirect 21 + github.com/goccy/go-json v0.10.2 // indirect 22 + github.com/gogo/protobuf v1.3.2 // indirect 23 + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 24 + github.com/google/uuid v1.6.0 // indirect 25 + github.com/gorilla/securecookie v1.1.2 // indirect 8 26 github.com/gorilla/websocket v1.5.3 // indirect 27 + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 28 + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 29 + github.com/hashicorp/golang-lru v1.0.2 // indirect 30 + github.com/ipfs/bbloom v0.0.4 // indirect 31 + github.com/ipfs/go-block-format v0.2.0 // indirect 32 + github.com/ipfs/go-cid v0.4.1 // indirect 33 + github.com/ipfs/go-datastore v0.6.0 // indirect 34 + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 35 + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 36 + github.com/ipfs/go-ipfs-util v0.0.3 // indirect 37 + github.com/ipfs/go-ipld-cbor v0.1.0 // indirect 38 + github.com/ipfs/go-ipld-format v0.6.0 // indirect 39 + github.com/ipfs/go-log v1.0.5 // indirect 40 + github.com/ipfs/go-log/v2 v2.5.1 // indirect 41 + github.com/ipfs/go-metrics-interface v0.0.1 // indirect 9 42 github.com/jackc/pgpassfile v1.0.0 // indirect 10 43 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 11 - github.com/jackc/pgx/v5 v5.7.4 12 - github.com/joho/godotenv v1.5.1 13 - github.com/rachel-mp4/lrc/lrc v0.0.0-20250408013928-75dc71a6060f // indirect 44 + github.com/jackc/puddle/v2 v2.2.2 // indirect 45 + github.com/jbenet/goprocess v0.1.4 // indirect 46 + github.com/klauspost/cpuid/v2 v2.2.7 // indirect 47 + github.com/lestrrat-go/blackmagic v1.0.2 // indirect 48 + github.com/lestrrat-go/httpcc v1.0.1 // indirect 49 + github.com/lestrrat-go/httprc v1.0.4 // indirect 50 + github.com/lestrrat-go/iter v1.0.2 // indirect 51 + github.com/lestrrat-go/option v1.0.1 // indirect 52 + github.com/mattn/go-isatty v0.0.20 // indirect 53 + github.com/minio/sha256-simd v1.0.1 // indirect 54 + github.com/mr-tron/base58 v1.2.0 // indirect 55 + github.com/multiformats/go-base32 v0.1.0 // indirect 56 + github.com/multiformats/go-base36 v0.2.0 // indirect 57 + github.com/multiformats/go-multibase v0.2.0 // indirect 58 + github.com/multiformats/go-multihash v0.2.3 // indirect 59 + github.com/multiformats/go-varint v0.0.7 // indirect 60 + github.com/opentracing/opentracing-go v1.2.0 // indirect 61 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 62 + github.com/rachel-mp4/lrcproto v0.0.0-20250527205756-58da8216f98c // indirect 63 + github.com/segmentio/asm v1.2.0 // indirect 64 + github.com/spaolacci/murmur3 v1.1.0 // indirect 65 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 66 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 67 + go.opentelemetry.io/otel v1.29.0 // indirect 68 + go.opentelemetry.io/otel/metric v1.29.0 // indirect 69 + go.opentelemetry.io/otel/trace v1.29.0 // indirect 70 + go.uber.org/atomic v1.11.0 // indirect 71 + go.uber.org/multierr v1.11.0 // indirect 72 + go.uber.org/zap v1.26.0 // indirect 14 73 golang.org/x/crypto v0.31.0 // indirect 74 + golang.org/x/sync v0.10.0 // indirect 75 + golang.org/x/sys v0.28.0 // indirect 15 76 golang.org/x/text v0.21.0 // indirect 77 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 78 + google.golang.org/protobuf v1.36.6 // indirect 79 + lukechampine.com/blake3 v1.2.1 // indirect 16 80 ) 17 - 18 - replace github.com/rachel-mp4/lrc/lrcd => ../../lrc/lrcd 19 - 20 - replace github.com/rachel-mp4/lrc/lrc => ../../lrc/lrc
+277 -4
server/go.sum
··· 1 + github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 + github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk= 4 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 5 + github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 6 + github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 7 + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 1 8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 - github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 9 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 11 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 + github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 13 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 14 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 15 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 16 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 17 + github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 18 + github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 19 + github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 20 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 21 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 22 + github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 23 + github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 24 + github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 25 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 26 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 27 + github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 28 + github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 29 + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 30 + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 31 + github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 32 + github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 + github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 34 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 37 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 38 + github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 39 + github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 40 + github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 41 + github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 4 42 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 5 43 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 44 + github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8= 45 + github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8= 46 + github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 47 + github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 48 + github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 49 + github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 50 + github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 51 + github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 52 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 53 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 54 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 55 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 56 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 57 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 58 + github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 59 + github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 60 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 61 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 62 + github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 63 + github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 64 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 65 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 66 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 67 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 68 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 69 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 70 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 71 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 72 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 73 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 74 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 75 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 76 + github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 77 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 78 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 79 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 80 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 6 81 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 7 82 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 8 83 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= ··· 11 86 github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 12 87 github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 13 88 github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 89 + github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 90 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 91 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 14 92 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 15 93 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 16 - github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 94 + github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 95 + github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 96 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 97 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 98 + github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 99 + github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 100 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 101 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 102 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 103 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 104 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 105 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 106 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 107 + github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 108 + github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 109 + github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 110 + github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 111 + github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 112 + github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 113 + github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 114 + github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 115 + github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 116 + github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 117 + github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 118 + github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 119 + github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 120 + github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 121 + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 122 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 123 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 124 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 125 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 126 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 127 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 128 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 129 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 130 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 131 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 132 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 133 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 134 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 135 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 136 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 137 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 138 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 139 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 140 + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 141 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 142 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 143 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 144 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 145 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 146 + github.com/rachel-mp4/lrcd v0.0.0-20250603192958-089ba44e79a5 h1:NMDkC4XYysiYebcoFDnsPdBVr8/NEuahKM6xqQJITp0= 147 + github.com/rachel-mp4/lrcd v0.0.0-20250603192958-089ba44e79a5/go.mod h1:Hn8xgJ2JwdiFJM5WjamVv4lRTwB6CdcqPjrCvJM7234= 148 + github.com/rachel-mp4/lrcproto v0.0.0-20250527205756-58da8216f98c h1:nOWeKeE7wph0IcwUyUBi0YBynUnAo4JW/J5DM88x4KM= 149 + github.com/rachel-mp4/lrcproto v0.0.0-20250527205756-58da8216f98c/go.mod h1:hQzO36tQELGbkmRnUtKeM6NMU34t79ZcTlhM+MO7pHw= 150 + github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 151 + github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 152 + github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 153 + github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 154 + github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 155 + github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 156 + github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 157 + github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 158 + github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 159 + github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 160 + github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 161 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 162 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 18 163 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 164 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 165 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 166 + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 19 167 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 168 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 169 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 170 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 - github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 22 - github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 171 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 172 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 173 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 174 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 175 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 176 + github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 177 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 178 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 179 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 180 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 181 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 182 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 183 + github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 184 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 185 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 186 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 187 + go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 188 + go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 189 + go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 190 + go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 191 + go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 192 + go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 193 + go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 194 + go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 195 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 196 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 197 + go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 198 + go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 199 + go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 200 + go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 201 + go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 202 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 203 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 204 + go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 205 + go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 206 + go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 207 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 208 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 209 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 210 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 211 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 212 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 213 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 214 + golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 23 215 golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 24 216 golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 217 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 218 + golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 219 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 220 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 221 + golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 222 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 223 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 224 + golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 225 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 226 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 227 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 228 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 229 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 230 + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 231 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 232 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 233 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 234 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 235 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 236 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 237 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 238 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 239 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 240 golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 26 241 golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 242 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 243 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 244 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 245 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 246 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 247 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 248 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 249 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 250 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 251 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 252 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 253 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 254 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 255 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 256 + golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 257 + golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 258 + golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 259 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 260 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 261 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 262 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 263 + golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 264 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 265 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 266 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 267 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 268 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 269 + golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 27 270 golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 28 271 golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 272 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 273 + golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 274 + golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 275 + golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 276 + golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 277 + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 278 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 279 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 280 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 281 + golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 282 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 283 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 284 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 285 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 286 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 287 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 288 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 289 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 290 + google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 291 + google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 29 292 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 293 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 294 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 295 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 296 + gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 297 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 298 + gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 30 299 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 300 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 301 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 302 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 303 + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 304 + lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 305 + lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+94 -27
server/internal/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 - "xcvr-backend/internal/types" 7 6 "os" 7 + "xcvr-backend/internal/types" 8 8 9 - "github.com/jackc/pgx/v5" 9 + "github.com/jackc/pgx/v5/pgxpool" 10 10 ) 11 11 12 - func Init() (*pgx.Conn, error) { 12 + type Store struct { 13 + pool *pgxpool.Pool 14 + } 15 + 16 + func Init() (*Store, error) { 17 + pool, err := initialize() 18 + return &Store{pool}, err 19 + } 20 + 21 + func (s *Store) Close() { 22 + s.pool.Close() 23 + } 24 + 25 + func initialize() (*pgxpool.Pool, error) { 13 26 dbuser := os.Getenv("POSTGRES_USER") 14 27 dbpass := os.Getenv("POSTGRES_PASSWORD") 15 28 dbhost := "localhost" 16 29 dbport := os.Getenv("POSTGRES_PORT") 17 30 dbdb := os.Getenv("POSTGRES_DB") 18 31 dburl := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dbuser, dbpass, dbhost, dbport, dbdb) 19 - conn, err := pgx.Connect(context.Background(), dburl) 32 + pool, err := pgxpool.New(context.Background(), dburl) 20 33 if err != nil { 21 - return conn, err 34 + return nil, err 22 35 } 23 - pingErr := conn.Ping(context.Background()) 36 + pingErr := pool.Ping(context.Background()) 24 37 if pingErr != nil { 25 - return conn, pingErr 38 + return nil, pingErr 26 39 } 27 40 fmt.Println("connected!") 28 - return conn, nil 41 + return pool, nil 42 + } 43 + 44 + 45 + func (s *Store) ResolveHandle(handle string, ctx context.Context) (string, error) { 46 + rows, err := s.pool.Query(ctx, ` 47 + SELECT 48 + h.did 49 + FROM did_handles h 50 + WHERE h.handle = $1 51 + LIMIT 1 52 + `, handle) 53 + if err != nil { 54 + return "", err 55 + } 56 + defer rows.Close() 57 + var did string 58 + for rows.Next() { 59 + err := rows.Scan(&did) 60 + if err != nil { 61 + return "", err 62 + } 63 + } 64 + return did, nil 29 65 } 30 66 31 - func GetMessages(channelURI string, limit int,ctx context.Context, db *pgx.Conn) ([]types.Message, error) { 32 - rows, err := db.Query(ctx, ` 67 + func (s *Store) GetMessages(channelURI string, limit int, ctx context.Context) ([]types.Message, error) { 68 + rows, err := s.pool.Query(ctx, ` 33 69 SELECT 34 70 m.uri, m.did, m.signet_uri, m.body, m.nick, m.color, m.posted_at 35 71 FROM messages m ··· 41 77 if err != nil { 42 78 return nil, err 43 79 } 44 - var msgs = make([]types.Message, 0, limit) 80 + defer rows.Close() 81 + var msgs = make([]types.Message, 0, limit) 45 82 for rows.Next() { 46 83 var msg types.Message 47 84 err := rows.Scan(&msg.URI, &msg.DID, &msg.SignetURI, &msg.Body, &msg.Nick, &msg.PostedAt) ··· 53 90 return msgs, nil 54 91 } 55 92 56 - func GetChannels(limit int, ctx context.Context, db *pgx.Conn) ([]types.Channel, error) { 57 - rows, err := db.Query(ctx, ` 58 - SELECT 59 - c.uri, c.did, c.host, c.title, c.topic, c.created_at 60 - FROM channels c 61 - ORDER BY s.message_id DESC 62 - LIMIT $2 63 - `, limit) 93 + func (s *Store) GetChannelURI(handle string, title string, ctx context.Context) (string, error) { 94 + rows, err := s.pool.Query(ctx, ` 95 + SELECT 96 + channels.uri 97 + FROM channels 98 + LEFT JOIN did_handles ON channels.did = did_handles.did 99 + WHERE channels.title = $1 AND did_handles.handle = $2 100 + ORDER BY channels.created_at DESC 101 + LIMIT 1 102 + `, title, handle) 103 + if err != nil { 104 + return "", err 105 + } 106 + defer rows.Close() 107 + var uri string 108 + rows.Next() 109 + err = rows.Scan(&uri) 110 + if err != nil { 111 + return "", err 112 + } 113 + return uri, nil 114 + } 115 + 116 + type URIHost struct { 117 + URI string 118 + Host string 119 + } 120 + 121 + func (s *Store) GetChannelURIs(ctx context.Context) ([]URIHost, error) { 122 + rows, err := s.pool.Query(ctx, ` 123 + SELECT 124 + channels.uri, 125 + channels.host 126 + FROM channels 127 + `) 64 128 if err != nil { 65 129 return nil, err 66 130 } 67 - var chans = make([]types.Channel, 0, limit) 131 + defer rows.Close() 132 + var urihosts = make([]URIHost, 0, 100) 68 133 for rows.Next() { 69 - var c types.Channel 70 - err := rows.Scan(&c.URI, &c.DID, &c.Host, &c.Title, &c.Topic, &c.CreatedAt) 134 + var urihost URIHost 135 + err := rows.Scan(&urihost.URI, &urihost.Host) 71 136 if err != nil { 72 137 return nil, err 73 138 } 74 - chans = append(chans, c) 139 + urihosts = append(urihosts, urihost) 75 140 } 76 - return chans, nil 141 + return urihosts, nil 77 142 } 78 143 79 - func GetChannelViews(limit int, ctx context.Context, db *pgx.Conn) ([]types.ChannelView, error) { 80 - rows, err := db.Query(ctx, ` 144 + func (s *Store) GetChannelViews(limit int, ctx context.Context) ([]types.ChannelView, error) { 145 + rows, err := s.pool.Query(ctx, ` 81 146 SELECT 82 147 channels.uri, 83 148 channels.host, ··· 99 164 if err != nil { 100 165 return nil, err 101 166 } 102 - var chans = make([]types.ChannelView, 0, limit) 167 + defer rows.Close() 168 + var chans = make([]types.ChannelView, 0, limit) 103 169 for rows.Next() { 104 170 var c types.ChannelView 105 171 var p types.ProfileView ··· 113 179 return chans, nil 114 180 } 115 181 182 +
+136
server/internal/db/oauth.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "xcvr-backend/internal/oauth" 7 + ) 8 + 9 + func (s *Store) StoreOAuthRequest(req *oauth.OAuthRequest, ctx context.Context) error { 10 + _, err := s.pool.Exec(ctx, ` 11 + INSERT INTO oauthrequests ( 12 + authserver_iss, 13 + state, 14 + did, 15 + pds_url, 16 + pkce_verifier, 17 + dpop_auth_server_nonce, 18 + dpop_private_jwk 19 + ) VALUES ($1, $2, $3, $4, $5, $6, $7)`, 20 + req.AuthserverIss, 21 + req.State, 22 + req.Did, 23 + req.PdsUrl, 24 + req.PkceVerifier, 25 + req.DpopAuthServerNonce, 26 + req.DpopPrivKey) 27 + return err 28 + } 29 + 30 + func (s *Store) StoreOAuthSession(session *oauth.Session, ctx context.Context) error { 31 + _, err := s.pool.Exec(ctx, ` 32 + INSERT INTO oauthsessions ( 33 + authserver_iss, 34 + state, 35 + did, 36 + pds_url, 37 + pkce_verifier, 38 + dpop_auth_server_nonce, 39 + dpop_private_jwk, 40 + dpop_pds_nonce, 41 + access_token, 42 + refresh_token, 43 + expiration 44 + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, 45 + session.AuthserverIss, 46 + session.State, 47 + session.Did, 48 + session.PdsUrl, 49 + session.PkceVerifier, 50 + session.DpopAuthServerNonce, 51 + session.DpopPrivKey, 52 + session.DpopPdsNonce, 53 + session.AccessToken, 54 + session.RefreshToken, 55 + session.Expiration) 56 + return err 57 + } 58 + 59 + func (s *Store) GetOauthRequest(state string, ctx context.Context) (*oauth.OAuthRequest, error) { 60 + rows, err := s.pool.Query(ctx, ` 61 + SELECT 62 + r.authserver_iss, 63 + r.did, 64 + r.pds_url, 65 + r.pkce_verifier 66 + r.dpop_auth_server_nonce, 67 + r.dpop_private_jwk 68 + FROM oauthrequests r 69 + WHERE r.state = $1 70 + LIMIT 1 71 + `, state) 72 + if err != nil { 73 + return nil, err 74 + } 75 + defer rows.Close() 76 + var req oauth.OAuthRequest 77 + ok := rows.Next() 78 + if !ok { 79 + return nil, errors.New("no rows") 80 + } 81 + err = rows.Scan(&req.AuthserverIss, &req.Did, &req.PdsUrl, &req.PkceVerifier, &req.DpopAuthServerNonce, &req.DpopPrivKey) 82 + if err != nil { 83 + return nil, err 84 + } 85 + return &req, nil 86 + } 87 + 88 + func (s *Store) GetOauthSesson(did string, ctx context.Context) (*oauth.Session, error) { 89 + rows, err := s.pool.Query(ctx, ` 90 + SELECT 91 + r.authserver_iss, 92 + r.did, 93 + r.pds_url, 94 + r.pkce_verifier 95 + r.dpop_auth_server_nonce, 96 + r.dpop_private_jwk, 97 + r.dpop_pds_nonce, 98 + r.access_token, 99 + r.refresh_token, 100 + r.expiration 101 + FROM oauthsessions r 102 + WHERE r.did = $1 103 + LIMIT 1 104 + `, did) 105 + if err != nil { 106 + return nil, err 107 + } 108 + defer rows.Close() 109 + var session oauth.Session 110 + ok := rows.Next() 111 + if !ok { 112 + return nil, errors.New("no rows") 113 + } 114 + err = rows.Scan( 115 + &session.AuthserverIss, 116 + &session.Did, 117 + &session.PdsUrl, 118 + &session.PkceVerifier, 119 + &session.DpopAuthServerNonce, 120 + &session.DpopPrivKey, 121 + &session.DpopPdsNonce, 122 + &session.AccessToken, 123 + &session.RefreshToken, 124 + &session.Expiration) 125 + if err != nil { 126 + return nil, err 127 + } 128 + return &session, nil 129 + } 130 + 131 + func (s *Store) DeleteOauthRequest(state string, ctx context.Context) error { 132 + _, err := s.pool.Exec(ctx, ` 133 + DELETE FROM oauthrequests WHERE r.state = $1 134 + `, state) 135 + return err 136 + }
+33 -74
server/internal/handler/handler.go
··· 1 1 package handler 2 2 3 3 import ( 4 - "encoding/json" 5 4 "net/http" 6 - "strconv" 5 + "os" 6 + "github.com/gorilla/sessions" 7 7 "xcvr-backend/internal/db" 8 8 "xcvr-backend/internal/log" 9 - "xcvr-backend/internal/model" 10 - 11 - "github.com/jackc/pgx/v5" 9 + "xcvr-backend/internal/oauth" 12 10 ) 13 11 14 12 type Handler struct { 15 - db *pgx.Conn 16 - router *http.ServeMux 17 - logger log.Logger 13 + db *db.Store 14 + sessionStore *sessions.CookieStore 15 + router *http.ServeMux 16 + logger log.Logger 17 + oauth *oauth.Service 18 18 } 19 19 20 - func New(conn *pgx.Conn, logger log.Logger) *Handler { 20 + func New(db *db.Store, logger log.Logger, oauth *oauth.Service) *Handler { 21 21 mux := http.NewServeMux() 22 - h := &Handler{conn, mux, logger} 23 - mux.HandleFunc("GET /lrc/{title}/ws", h.acceptWebsocket) 24 - mux.HandleFunc("GET /lrc/{user}/{title}/ws", h.acceptWebsocketUser) 25 - mux.HandleFunc("GET /xrpc/org.xcvr.feed.getChannels", h.getChannels) 26 - mux.HandleFunc("GET /xrpc/org.xcvr.lrc.getMessages", h.getMessages) 22 + sessionStore := sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) 23 + h := &Handler{db, sessionStore, mux, logger, oauth} 24 + // lrc handlers 25 + mux.HandleFunc("GET /lrc/{user}/{rkey}/ws", h.acceptWebsocket) 27 26 mux.HandleFunc("POST /lrc/channel", postChannel) 28 27 mux.HandleFunc("POST /lrc/message", postMessage) 28 + // lexicon handlers 29 + mux.HandleFunc("GET /xrpc/org.xcvr.feed.getChannels", h.getChannels) 30 + mux.HandleFunc("GET /xrpc/org.xcvr.lrc.getMessages", h.getMessages) 31 + mux.HandleFunc("GET /xrpc/org.xcvr.actor.resolveChannel", h.resolveChannel) 32 + // backend metadata handlers 33 + mux.HandleFunc(clientMetadataPath(), h.serveClientMetadata) 34 + mux.HandleFunc(clientTOSPath(), h.serveTOS) 35 + mux.HandleFunc(clientPolicyPath(), h.servePolicy) 36 + // oauth handlers 37 + mux.HandleFunc(oauthJWKSPath(), h.serveJWKS) 38 + mux.HandleFunc("POST /oauth/login", h.oauthLogin) 39 + mux.HandleFunc("GET /oauth", h.getSession) 40 + mux.HandleFunc(oauthCallbackPath(), h.oauthCallback) 29 41 return h 30 42 } 31 43 32 - func (h *Handler) acceptWebsocket(w http.ResponseWriter, r *http.Request) { 33 - title := r.PathValue("title") 34 - f, err := model.GetWSHandlerFrom(title, h.db) 35 - if err != nil { 36 - http.NotFound(w, r) 37 - h.logger.Deprintf("couldn't find server %s", title) 38 - return 39 - } 40 - f(w, r) 44 + func (h *Handler) badRequest(w http.ResponseWriter, err error) { 45 + h.logger.Deprintln(err.Error()) 46 + http.Error(w, `{"error":"Invalid JSON","message":"Could not parse request body"}`, http.StatusBadRequest) 41 47 } 42 48 43 - func (h *Handler) acceptWebsocketUser(w http.ResponseWriter, r *http.Request) { 44 - title := r.PathValue("title") 45 - user := r.PathValue("user") 46 - f, err := model.GetWSHandlerFrom(title, h.db) 47 - if err != nil { 48 - http.NotFound(w, r) 49 - h.logger.Deprintf("couldn't find user %s's server %s", user, title) 50 - return 51 - } 52 - f(w, r) 53 - } 54 - 55 - 56 - func (h *Handler) getMessages(w http.ResponseWriter, r *http.Request) { 57 - 58 - } 59 - 60 - func (h *Handler) getChannels(w http.ResponseWriter, r *http.Request) { 61 - limitstr := r.URL.Query().Get("limit") 62 - limit := 50 63 - if limitstr != "" { 64 - l, err := strconv.Atoi(limitstr) 65 - if err == nil { 66 - limit = max(min(l, 100),1) 67 - } 68 - } 69 - cvs, err := db.GetChannelViews(limit, r.Context(), h.db) 70 - if err != nil { 71 - serverError(w) 72 - h.logger.Printf("db.GetChannels failed! %s", err.Error()) 73 - return 74 - } 75 - encoder := json.NewEncoder(w) 76 - encoder.Encode(cvs) 77 - } 78 - 79 - func postChannel(w http.ResponseWriter, r *http.Request) { 80 - 81 - } 82 - 83 - func postMessage(w http.ResponseWriter, r *http.Request) { 84 - 85 - } 86 - 87 - func badRequest(w http.ResponseWriter) { 88 - http.Error(w, `{"error":"Invalid JSON","message":"Could not parse request body"}`,http.StatusBadRequest) 89 - } 90 - 91 - func serverError(w http.ResponseWriter) { 92 - http.Error(w, `{"error":"Internal server error","message":"Something went wrong"}`,http.StatusInternalServerError) 49 + func (h *Handler) serverError(w http.ResponseWriter, err error) { 50 + h.logger.Println(err.Error()) 51 + http.Error(w, `{"error":"Internal server error","message":"Something went wrong"}`, http.StatusInternalServerError) 93 52 } 94 53 95 54 func (h *Handler) WithCORSAll() http.Handler { ··· 104 63 } 105 64 h.router.ServeHTTP(w, r) 106 65 }) 107 - } 66 + }
+55
server/internal/handler/lexiconHandlers.go
··· 1 + package handler 2 + 3 + import ( 4 + "xcvr-backend/internal/types" 5 + "errors" 6 + "strconv" 7 + "fmt" 8 + "encoding/json" 9 + "net/http" 10 + ) 11 + 12 + func (h *Handler) getChannels(w http.ResponseWriter, r *http.Request) { 13 + limitstr := r.URL.Query().Get("limit") 14 + limit := 50 15 + if limitstr != "" { 16 + l, err := strconv.Atoi(limitstr) 17 + if err == nil { 18 + limit = max(min(l, 100), 1) 19 + } 20 + } 21 + cvs, err := h.db.GetChannelViews(limit, r.Context()) 22 + if err != nil { 23 + h.serverError(w, err) 24 + h.logger.Printf("db.GetChannels failed! %s", err.Error()) 25 + return 26 + } 27 + encoder := json.NewEncoder(w) 28 + encoder.Encode(cvs) 29 + } 30 + 31 + func (h *Handler) getMessages(w http.ResponseWriter, r *http.Request) { 32 + 33 + } 34 + 35 + func (h *Handler) resolveChannel(w http.ResponseWriter, r *http.Request) { 36 + handle := r.URL.Query().Get("handle") 37 + did := r.URL.Query().Get("did") 38 + rkey := r.URL.Query().Get("rkey") 39 + if did == "" { 40 + if handle == "" { 41 + h.badRequest(w, errors.New("did not provide did or handle")) 42 + return 43 + } 44 + var err error 45 + did, err = h.db.ResolveHandle(handle, r.Context()) 46 + if err != nil { 47 + h.serverError(w, err) 48 + return 49 + } 50 + } 51 + url := fmt.Sprintf("/lrc/%s/%s/ws", did, rkey) 52 + rchanres := types.ResolveChannelResponse{URL: url} 53 + encoder := json.NewEncoder(w) 54 + encoder.Encode(rchanres) 55 + }
+28
server/internal/handler/lrcHandlers.go
··· 1 + package handler 2 + import ( 3 + "net/http" 4 + "xcvr-backend/internal/model" 5 + "fmt" 6 + ) 7 + 8 + func (h *Handler) acceptWebsocket(w http.ResponseWriter, r *http.Request) { 9 + rkey := r.PathValue("rkey") 10 + user := r.PathValue("user") 11 + uri := fmt.Sprintf("at://%s/org.xcvr.feed.channel/%s", user, rkey) 12 + f, err := model.GetWSHandlerFrom(uri) 13 + if err != nil { 14 + http.NotFound(w, r) 15 + h.logger.Deprintf("couldn't find user %s's server %s", user, rkey) 16 + h.logger.Println(err.Error()) 17 + return 18 + } 19 + f(w, r) 20 + } 21 + 22 + func postChannel(w http.ResponseWriter, r *http.Request) { 23 + 24 + } 25 + 26 + func postMessage(w http.ResponseWriter, r *http.Request) { 27 + 28 + }
+36
server/internal/handler/metadataHandlers.go
··· 1 + package handler 2 + import ( 3 + "fmt" 4 + "net/http" 5 + "encoding/json" 6 + "os" 7 + "xcvr-backend/internal/oauth" 8 + ) 9 + 10 + func (h *Handler) serveClientMetadata(w http.ResponseWriter, r *http.Request) { 11 + metadata := oauth.GetClientMetadata() 12 + encoder := json.NewEncoder(w) 13 + encoder.Encode(metadata) 14 + } 15 + 16 + func (h *Handler) serveTOS(w http.ResponseWriter, r *http.Request) { 17 + fmt.Fprint(w, "be normal be normal be normal be normal be normal be normal be normal") 18 + } 19 + func (h *Handler) servePolicy(w http.ResponseWriter, r *http.Request) { 20 + fmt.Fprint(w, "i'll be normal i'll be normal i'll be normal i'll be normal") 21 + } 22 + 23 + func clientMetadataPath() string { 24 + mp := os.Getenv("MY_METADATA_PATH") 25 + return fmt.Sprintf("GET %s", mp) 26 + } 27 + 28 + func clientTOSPath() string { 29 + mp := os.Getenv("MY_TOS_PATH") 30 + return fmt.Sprintf("GET %s", mp) 31 + } 32 + 33 + func clientPolicyPath() string { 34 + mp := os.Getenv("MY_POLICY_PATH") 35 + return fmt.Sprintf("GET %s", mp) 36 + }
+144
server/internal/handler/oauthHandlers.go
··· 1 + package handler 2 + 3 + import ( 4 + "encoding/json" 5 + "errors" 6 + "fmt" 7 + "github.com/gorilla/sessions" 8 + "net/http" 9 + "net/url" 10 + "os" 11 + "xcvr-backend/internal/oauth" 12 + ) 13 + 14 + func (h *Handler) serveJWKS(w http.ResponseWriter, r *http.Request) { 15 + pubKey, err := oauth.GetJWKS() 16 + if err != nil { 17 + h.serverError(w, err) 18 + } 19 + encoder := json.NewEncoder(w) 20 + encoder.Encode(pubKey) 21 + } 22 + 23 + func (h *Handler) oauthLogin(w http.ResponseWriter, r *http.Request) { 24 + err := r.ParseForm() 25 + if err != nil { 26 + h.badRequest(w, err) 27 + return 28 + } 29 + handle := r.FormValue("handle") 30 + req, res, err := h.oauth.StartAuthFlow(r.Context(), handle) 31 + if err != nil { 32 + h.serverError(w, err) 33 + return 34 + } 35 + err = h.db.StoreOAuthRequest(req, r.Context()) 36 + if err != nil { 37 + h.serverError(w, err) 38 + return 39 + } 40 + u, _ := url.Parse(res.AuthzEndpoint) 41 + u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(oauth.GetClientMetadata().ClientId), res.RequestUri) 42 + 43 + session, _ := h.sessionStore.Get(r, "oauthsession") 44 + session.Values = map[any]any{} 45 + 46 + session.Options = &sessions.Options{ 47 + Path: "/", 48 + MaxAge: 300, 49 + HttpOnly: true, 50 + } 51 + session.Values["oauth_state"] = res.State 52 + session.Values["oauth_did"] = res.DID 53 + err = session.Save(r, w) 54 + if err != nil { 55 + h.serverError(w, err) 56 + return 57 + } 58 + http.Redirect(w, r, u.String(), http.StatusFound) 59 + } 60 + 61 + func (h *Handler) oauthCallback(w http.ResponseWriter, r *http.Request) { 62 + resState := r.FormValue("state") 63 + resIss := r.FormValue("iss") 64 + resCode := r.FormValue("code") 65 + session, err := h.sessionStore.Get(r, "oauthsession") 66 + if err != nil { 67 + h.serverError(w, err) 68 + return 69 + } 70 + if resState == "" || resIss == "" || resCode == "" { 71 + h.badRequest(w, errors.New("did not provide one of resState, resIss, resCode")) 72 + return 73 + } 74 + sessionState, ok := session.Values["oauth_state"].(string) 75 + if !ok { 76 + h.serverError(w, errors.New("oauth_state not found in session")) 77 + return 78 + } 79 + if resState != sessionState { 80 + h.serverError(w, errors.New("resState and sessionState do not match!")) 81 + return 82 + } 83 + params := oauth.CallbackParams{ 84 + State: resState, 85 + Iss: resIss, 86 + Code: resCode, 87 + } 88 + req, err := h.db.GetOauthRequest(resState, r.Context()) 89 + if err != nil { 90 + h.serverError(w, err) 91 + return 92 + } 93 + OauthSession, err := h.oauth.OauthCallback(r.Context(), req, params) 94 + if err != nil { 95 + h.serverError(w, err) 96 + return 97 + } 98 + err = h.db.DeleteOauthRequest(resState, r.Context()) 99 + if err != nil { 100 + h.serverError(w, err) 101 + return 102 + } 103 + err = h.db.StoreOAuthSession(OauthSession, r.Context()) 104 + if err != nil { 105 + h.serverError(w, err) 106 + return 107 + } 108 + 109 + session.Options = &sessions.Options{ 110 + Path: "/", 111 + MaxAge: 86400 * 7, 112 + HttpOnly: true, 113 + } 114 + session.Values = map[any]any{} 115 + session.Values["did"] = req.Did 116 + err = session.Save(r, w) 117 + if err != nil { 118 + h.serverError(w, err) 119 + return 120 + } 121 + http.Redirect(w, r, "/", http.StatusFound) 122 + } 123 + 124 + func oauthCallbackPath() string { 125 + mp := os.Getenv("MY_OAUTH_CALLBACK") 126 + return fmt.Sprintf("GET %s", mp) 127 + } 128 + 129 + func oauthJWKSPath() string { 130 + mp := os.Getenv("MY_JWKS_PATH") 131 + return fmt.Sprintf("GET %s", mp) 132 + } 133 + 134 + func (h *Handler) getSession(w http.ResponseWriter, r *http.Request) { 135 + session, _ := h.sessionStore.Get(r, "oauthsession") 136 + did, ok := session.Values["did"].(string) 137 + if !ok || did == "" { 138 + http.Error(w, "not authenticated", http.StatusUnauthorized) 139 + return 140 + } 141 + json.NewEncoder(w).Encode(map[string]any{ 142 + "did": did, 143 + }) 144 + }
+36 -68
server/internal/model/channel.go
··· 1 1 package model 2 2 3 3 import ( 4 + "context" 4 5 "errors" 5 6 "net/http" 6 - "sync" 7 - "time" 8 - "xcvr-backend/internal/types" 7 + "os" 8 + "xcvr-backend/internal/db" 9 9 10 - "github.com/jackc/pgx/v5" 11 - "github.com/rachel-mp4/lrc/lrcd/pkg/lrcd" 10 + "github.com/rachel-mp4/lrcd" 12 11 ) 13 12 14 13 var ( 15 - channelsMu sync.Mutex 16 - channels = make([]channel, 0) 14 + validServer map[string]bool 17 15 uriToServer = make(map[string]*lrcd.Server) 18 - didToPView = make(map[string]*pView) 19 16 ) 20 17 21 - type pView struct { 22 - profileView types.ProfileView 23 - lastUpdated time.Time 18 + func GetWSHandlerFrom(uri string) (http.HandlerFunc, error) { 19 + server, err := getServer(uri) 20 + if err != nil { 21 + return nil, err 22 + } 23 + return server.WSHandler(), nil 24 24 } 25 25 26 - type channel struct { 27 - Title string `json:"title"` 28 - Topic string `json:"topic"` 29 - CreatedAt string `json:"createdAt"` 30 - Host string `json:"host"` 26 + func Init(store *db.Store) { 27 + uris, err := store.GetChannelURIs(context.Background()) 28 + if err != nil { 29 + panic(err) 30 + } 31 + validServer = make(map[string]bool, len(uris)) 32 + myid := os.Getenv("MY_IDENTITY") 33 + for _, uri := range uris { 34 + validServer[uri.URI] = (uri.Host == myid) 35 + } 31 36 } 32 37 33 - func GetWSHandlerFrom(uri string, db *pgx.Conn) (http.HandlerFunc, error) { 38 + func getServer(uri string) (*lrcd.Server, error) { 39 + if !validServer[uri] { 40 + return nil, errors.New("Not a valid server") 41 + } 34 42 server, ok := uriToServer[uri] 35 43 if !ok { 36 - return nil, errors.New("channel does not exist") 44 + var err error 45 + server, err = lrcd.NewServer(lrcd.WithLogging(os.Stdout,true)) 46 + if err != nil { 47 + return nil, errors.New("Error creating server") 48 + } 49 + uriToServer[uri] = server 50 + err = server.Start() 51 + if err != nil { 52 + return nil, errors.New("Error starting server") 53 + } 37 54 } 38 - return server.WSHandler(), nil 55 + return server, nil 39 56 } 40 57 41 - // func CreateChannel(title string, topic string) error { 42 - // c := channel{Title: title, Topic: topic} 43 - // _, err := createChannel(c) 44 - // return err 45 - // } 46 - 47 - // func createChannel(c channel) (channel, error) { 48 - // options := []lrcd.Option{ 49 - // lrcd.WithWelcome(c.Title), 50 - // lrcd.WithLogging(os.Stdout, true), 51 - // } 52 - // ec := make(chan struct{}) 53 - 54 - // server, err := lrcd.NewServer(options...) 55 - 56 - // if err != nil { 57 - // fmt.Println(err.Error()) 58 - // return channel{}, err 59 - // } 60 - // fmt.Println("created", c.Title) 61 - 62 - // err = server.Start() 63 - // if err != nil { 64 - // fmt.Println(err.Error()) 65 - // return channel{}, err 66 - // } 67 - // fmt.Println("started", c.Title) 68 - 69 - // channelsMu.Lock() 70 - // defer channelsMu.Unlock() 71 - // uriToServer[c.Band] = server 72 - // channels = append(channels, c) 73 - // if withDelete { 74 - // go func() { 75 - // <-ec 76 - // channelsMu.Lock() 77 - // idx := slices.Index(channels, c) 78 - // channels = slices.Delete(channels, idx, idx+1) 79 - // err = bandToServer[c.Band].Stop() 80 - // if err != nil { 81 - // fmt.Println(err.Error()) 82 - // } 83 - // delete(bandToServer, c.Band) 84 - // channelsMu.Unlock() 85 - // fmt.Println("deleted", c.Band) 86 - // }() 87 - // } 88 - // return c, nil 89 - // }
+27
server/internal/oauth/jwks.go
··· 1 + package oauth 2 + 3 + import ( 4 + "os" 5 + "github.com/haileyok/atproto-oauth-golang/helpers" 6 + "github.com/lestrrat-go/jwx/v2/jwk" 7 + ) 8 + 9 + var ( 10 + key *jwk.Key 11 + ) 12 + 13 + func GetJWKS() (*jwk.Key, error) { 14 + if key != nil { 15 + return key, nil 16 + } 17 + b, err := os.ReadFile("../jwk.json") 18 + if err != nil { 19 + return nil, err 20 + } 21 + k, err := helpers.ParseJWKFromBytes(b) 22 + if err != nil { 23 + return nil, err 24 + } 25 + key = &k 26 + return key, nil 27 + }
+85
server/internal/oauth/metadata.go
··· 1 + package oauth 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + ) 7 + 8 + var ( 9 + mi = os.Getenv("MY_IDENTITY") 10 + mp = os.Getenv("MY_METADATA_PATH") 11 + clientMetadata *ClientMetadata 12 + ) 13 + 14 + type ClientMetadata struct { 15 + ClientId string `json:"client_id"` 16 + ClientName string `json:"client_name"` 17 + ClientUri string `json:"client_uri"` 18 + LogoUri string `json:"logo_uri"` 19 + TosUri string `json:"tos_uri"` 20 + PolicyUrl string `json:"policy_url"` 21 + RedirectUris []string `json:"redirect_uris"` 22 + GrantTypes []string `json:"grant_types"` 23 + ResponseTypes []string `json:"response_types"` 24 + ApplicationType string `json:"application_type"` 25 + DPOPBoundAccessTokens bool `json:"dpop_bound_access_tokens"` 26 + JWKSUri string `json:"jwks_uri"` 27 + Scope string `json:"scope"` 28 + TokenEndpointAuthMethod string `json:"token_endpoing_auth_method"` 29 + TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"` 30 + } 31 + 32 + func GetClientMetadata() ClientMetadata { 33 + if clientMetadata == nil { 34 + clientMetadata = &ClientMetadata{ 35 + ClientId: getClientId(), 36 + ClientName: getClientName(), 37 + ClientUri: getClientUri(), 38 + LogoUri: getLogoUri(), 39 + TosUri: getTOSUri(), 40 + PolicyUrl: getPolicyUri(), 41 + RedirectUris: []string{getOauthCallback()}, 42 + GrantTypes: []string{"authorization_code", "refresh_token"}, 43 + ResponseTypes: []string{"code"}, 44 + ApplicationType: "web", 45 + DPOPBoundAccessTokens: true, 46 + JWKSUri: getJWKSUri(), 47 + Scope: "atproto transition:generic", 48 + TokenEndpointAuthMethod: "private_key_jwt", 49 + TokenEndpointAuthSigningAlg: "ES256", 50 + } 51 + } 52 + return *clientMetadata 53 + } 54 + 55 + func getClientId() string { 56 + return fmt.Sprintf("https://%s%s", mi, mp) 57 + } 58 + 59 + func getClientName() string { 60 + return os.Getenv("MY_NAME") 61 + } 62 + 63 + func getClientUri() string { 64 + return fmt.Sprintf("https://%s", mi) 65 + } 66 + 67 + func getLogoUri() string { 68 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_LOGO_PATH")) 69 + } 70 + 71 + func getTOSUri() string { 72 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_TOS_PATH")) 73 + } 74 + 75 + func getPolicyUri() string { 76 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_POLICY_PATH")) 77 + } 78 + 79 + func getOauthCallback() string { 80 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_OAUTH_CALLBACK")) 81 + } 82 + 83 + func getJWKSUri() string { 84 + return fmt.Sprintf("%s%s", getClientUri(), os.Getenv("MY_JWKS_PATH")) 85 + }
+226
server/internal/oauth/service.go
··· 1 + package oauth 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "io" 9 + "time" 10 + "net/http" 11 + "net/url" 12 + "strings" 13 + 14 + atoauth "github.com/haileyok/atproto-oauth-golang" 15 + "github.com/haileyok/atproto-oauth-golang/helpers" 16 + "github.com/lestrrat-go/jwx/v2/jwk" 17 + ) 18 + 19 + type Service struct { 20 + oauth *atoauth.Client 21 + http *http.Client 22 + keys *jwk.Key 23 + } 24 + 25 + func NewService(httpClient *http.Client) (*Service, error) { 26 + key, err := GetJWKS() 27 + if err != nil { 28 + return nil, err 29 + } 30 + cid := getClientId() 31 + cbu := getOauthCallback() 32 + cli, err := atoauth.NewClient(atoauth.ClientArgs{ 33 + ClientJwk: *key, 34 + ClientId: cid, 35 + RedirectUri: cbu, 36 + }) 37 + if err != nil { 38 + return nil, err 39 + } 40 + return &Service{ 41 + oauth: cli, 42 + http: httpClient, 43 + keys: key, 44 + }, nil 45 + } 46 + 47 + type OauthFlowResult struct { 48 + AuthzEndpoint string 49 + State string 50 + DID string 51 + RequestUri string 52 + } 53 + 54 + type OAuthRequest struct { 55 + ID uint 56 + AuthserverIss string 57 + State string 58 + Did string 59 + PdsUrl string 60 + PkceVerifier string 61 + DpopAuthServerNonce string 62 + DpopPrivKey string 63 + } 64 + 65 + type CallbackParams struct { 66 + Iss string 67 + State string 68 + Code string 69 + } 70 + 71 + type Session struct { 72 + OAuthRequest 73 + DpopPdsNonce string 74 + AccessToken string 75 + RefreshToken string 76 + Expiration time.Time 77 + } 78 + 79 + func (s *Service) StartAuthFlow(ctx context.Context, handle string) (*OAuthRequest, *OauthFlowResult, error) { 80 + did, err := s.resolveHandle(handle) 81 + if err != nil { 82 + return nil, nil, err 83 + } 84 + dpopPrivKey, err := helpers.GenerateKey(nil) 85 + if err != nil { 86 + return nil, nil, err 87 + } 88 + dpopPrivKeyJson, err := json.Marshal(dpopPrivKey) 89 + if err != nil { 90 + return nil, nil, err 91 + } 92 + parResp, metadata, service, err := s.makeOAuthRequest(ctx, did, handle, dpopPrivKey) 93 + if err != nil { 94 + return nil, nil, err 95 + } 96 + oauthReq := OAuthRequest{ 97 + AuthserverIss: metadata.Issuer, 98 + State: parResp.State, 99 + Did: did, 100 + PkceVerifier: parResp.PkceVerifier, 101 + DpopAuthServerNonce: parResp.DpopAuthserverNonce, 102 + DpopPrivKey: string(dpopPrivKeyJson), 103 + PdsUrl: service, 104 + } 105 + oauthFlowResult := OauthFlowResult{ 106 + AuthzEndpoint: metadata.AuthorizationEndpoint, 107 + State: parResp.State, 108 + DID: did, 109 + RequestUri: parResp.RequestUri, 110 + } 111 + return &oauthReq, &oauthFlowResult, nil 112 + 113 + } 114 + 115 + func (s *Service) makeOAuthRequest(ctx context.Context, did string, handle string, dpop jwk.Key) (resp *atoauth.SendParAuthResponse, meta *atoauth.OauthAuthorizationMetadata, service string, err error) { 116 + service, err = s.resolveService(ctx, did) 117 + if err != nil { 118 + return 119 + } 120 + authserver, err := s.oauth.ResolvePdsAuthServer(ctx, service) 121 + if err != nil { 122 + return 123 + } 124 + meta, err = s.oauth.FetchAuthServerMetadata(ctx, service) 125 + if err != nil { 126 + return 127 + } 128 + resp, err = s.oauth.SendParAuthRequest(ctx, authserver, meta, handle, "atproto transition:generic", dpop) 129 + return 130 + } 131 + 132 + func (s *Service) resolveService(ctx context.Context, did string) (string, error) { 133 + type Identity struct { 134 + Service []struct { 135 + ID string `json:"id"` 136 + Type string `json:"type"` 137 + ServiceEndpoint string `json:"serviceEndpoint"` 138 + } `json:"service"` 139 + } 140 + var url string 141 + if strings.HasPrefix(did, "did:plc:") { 142 + url = fmt.Sprintf("https://plc.directory/%s", did) 143 + } else if strings.HasPrefix(did, "did:web:") { 144 + url = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:")) 145 + } else { 146 + return "", errors.New("did type not supported") 147 + } 148 + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 149 + if err != nil { 150 + return "", err 151 + } 152 + resp, err := s.http.Do(req) 153 + if err != nil { 154 + return "", err 155 + } 156 + defer resp.Body.Close() 157 + if resp.StatusCode != 200 { 158 + return "", errors.New("could not resolve did to service") 159 + } 160 + b, err := io.ReadAll(resp.Body) 161 + if err != nil { 162 + return "", err 163 + } 164 + var identity Identity 165 + err = json.Unmarshal(b, &identity) 166 + if err != nil { 167 + return "", err 168 + } 169 + var service *string 170 + for _, svc := range identity.Service { 171 + if svc.ID == "#atproto_pds" { 172 + service = &svc.ServiceEndpoint 173 + } 174 + } 175 + if service == nil { 176 + return "", errors.New("could not find atproto_pds service in resolved did's services") 177 + } 178 + return *service, nil 179 + } 180 + 181 + func (s *Service) resolveHandle(handle string) (string, error) { 182 + params := url.Values{ 183 + "handle": []string{handle}, 184 + } 185 + reqUrl := "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?" + params.Encode() 186 + resp, err := s.http.Get(reqUrl) 187 + if err != nil { 188 + return "", err 189 + } 190 + defer resp.Body.Close() 191 + 192 + type did struct { 193 + Did string 194 + } 195 + b, err := io.ReadAll(resp.Body) 196 + if err != nil { 197 + return "", err 198 + } 199 + var resDid did 200 + err = json.Unmarshal(b, &resDid) 201 + if err != nil { 202 + return "", err 203 + } 204 + return resDid.Did, nil 205 + } 206 + 207 + func (s *Service) OauthCallback(ctx context.Context, oauthRequest *OAuthRequest, params CallbackParams) (*Session, error) { 208 + jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivKey)) 209 + if err != nil { 210 + return nil, err 211 + } 212 + initialTokenResp, err := s.oauth.InitialTokenRequest(ctx, params.Code, params.Iss, oauthRequest.PkceVerifier, oauthRequest.DpopAuthServerNonce, jwk) 213 + if err != nil { 214 + return nil, err 215 + } 216 + if initialTokenResp.Scope != "atproto transition:generic" { 217 + return nil, errors.New(fmt.Sprintf("incorrect scope: %s", initialTokenResp.Scope)) 218 + } 219 + oauthSession := Session{ 220 + OAuthRequest: *oauthRequest, 221 + AccessToken: initialTokenResp.AccessToken, 222 + RefreshToken: initialTokenResp.RefreshToken, 223 + Expiration: time.Now().Add(time.Duration(int(time.Second) * int(initialTokenResp.ExpiresIn))), 224 + } 225 + return &oauthSession, nil 226 + }
+10
server/internal/types/lexicons.go
··· 41 41 IndexedAt time.Time 42 42 } 43 43 44 + type ResolveChannelRequest struct { 45 + DID *string `json:"did,omitempty"` 46 + Handle *string `json:"handle,omitempty"` 47 + Rkey string `json:"rkey"` 48 + } 49 + 50 + type ResolveChannelResponse struct { 51 + URL string `json:"url"` 52 + } 53 + 44 54 type GetChannelRequest struct { 45 55 Limit *int `json:"limit,omitempty"` 46 56 Cursor *string `json:"cursor,omitempty"`