collection of golang services under the Red Dwarf umbrella server.reddwarf.app
bluesky reddwarf microcosm appview
15
fork

Configure Feed

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

sticket init

whey.party 313532d7

+672
+213
auth/auth.go
··· 1 + package auth 2 + 3 + // taken from https://github.com/jazware/go-bsky-feed-generator 4 + // which doesnt seem to be published? 5 + import ( 6 + "context" 7 + "fmt" 8 + "net/http" 9 + "strings" 10 + "time" 11 + 12 + "github.com/bluesky-social/indigo/atproto/identity" 13 + "github.com/bluesky-social/indigo/atproto/syntax" 14 + es256k "github.com/ericvolp12/jwt-go-secp256k1" 15 + "github.com/gin-gonic/gin" 16 + "github.com/golang-jwt/jwt" 17 + lru "github.com/hashicorp/golang-lru/arc/v2" 18 + "github.com/prometheus/client_golang/prometheus" 19 + "github.com/prometheus/client_golang/prometheus/promauto" 20 + "gitlab.com/yawning/secp256k1-voi/secec" 21 + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 22 + "go.opentelemetry.io/otel" 23 + "go.opentelemetry.io/otel/attribute" 24 + "golang.org/x/time/rate" 25 + ) 26 + 27 + type KeyCacheEntry struct { 28 + UserDID string 29 + Key any 30 + ExpiresAt time.Time 31 + } 32 + 33 + // Initialize Prometheus Metrics for cache hits and misses 34 + var cacheHits = promauto.NewCounterVec(prometheus.CounterOpts{ 35 + Name: "feedgen_auth_cache_hits_total", 36 + Help: "The total number of cache hits", 37 + }, []string{"cache_type"}) 38 + 39 + var cacheMisses = promauto.NewCounterVec(prometheus.CounterOpts{ 40 + Name: "feedgen_auth_cache_misses_total", 41 + Help: "The total number of cache misses", 42 + }, []string{"cache_type"}) 43 + 44 + var cacheSize = promauto.NewGaugeVec(prometheus.GaugeOpts{ 45 + Name: "feedgen_auth_cache_size_bytes", 46 + Help: "The size of the cache in bytes", 47 + }, []string{"cache_type"}) 48 + 49 + type Auth struct { 50 + KeyCache *lru.ARCCache[string, KeyCacheEntry] 51 + KeyCacheTTL time.Duration 52 + ServiceDID string 53 + Dir *identity.CacheDirectory 54 + } 55 + 56 + // NewAuth creates a new Auth instance with the given key cache size and TTL 57 + // The PLC Directory URL is also required, as well as the DID of the service 58 + // for JWT audience validation 59 + // The key cache is used to cache the public keys of users for a given TTL 60 + // The PLC Directory URL is used to fetch the public keys of users 61 + // The service DID is used to validate the audience of JWTs 62 + // The HTTP client is used to make requests to the PLC Directory 63 + // A rate limiter is used to limit the number of requests to the PLC Directory 64 + func NewAuth( 65 + keyCacheSize int, 66 + keyCacheTTL time.Duration, 67 + requestsPerSecond int, 68 + serviceDID string, 69 + ) (*Auth, error) { 70 + keyCache, err := lru.NewARC[string, KeyCacheEntry](keyCacheSize) 71 + if err != nil { 72 + return nil, fmt.Errorf("Failed to create key cache: %v", err) 73 + } 74 + 75 + // Initialize the HTTP client with OpenTelemetry instrumentation 76 + client := http.Client{ 77 + Transport: otelhttp.NewTransport(http.DefaultTransport), 78 + } 79 + 80 + baseDir := identity.BaseDirectory{ 81 + PLCURL: identity.DefaultPLCURL, 82 + PLCLimiter: rate.NewLimiter(rate.Limit(float64(requestsPerSecond)), 1), 83 + HTTPClient: client, 84 + TryAuthoritativeDNS: true, 85 + // primary Bluesky PDS instance only supports HTTP resolution method 86 + SkipDNSDomainSuffixes: []string{".bsky.social"}, 87 + } 88 + dir := identity.NewCacheDirectory(&baseDir, keyCacheSize, keyCacheTTL, time.Minute*2, keyCacheTTL) 89 + 90 + return &Auth{ 91 + KeyCache: keyCache, 92 + KeyCacheTTL: keyCacheTTL, 93 + ServiceDID: serviceDID, 94 + Dir: &dir, 95 + }, nil 96 + } 97 + 98 + func (auth *Auth) GetClaimsFromAuthHeader(ctx context.Context, authHeader string, claims jwt.Claims) error { 99 + tracer := otel.Tracer("auth") 100 + ctx, span := tracer.Start(ctx, "Auth:GetClaimsFromAuthHeader") 101 + defer span.End() 102 + 103 + if authHeader == "" { 104 + span.End() 105 + return fmt.Errorf("No Authorization header provided") 106 + } 107 + 108 + authHeaderParts := strings.Split(authHeader, " ") 109 + if len(authHeaderParts) != 2 { 110 + return fmt.Errorf("Invalid Authorization header") 111 + } 112 + 113 + if authHeaderParts[0] != "Bearer" { 114 + return fmt.Errorf("Invalid Authorization header (expected Bearer)") 115 + } 116 + 117 + accessToken := authHeaderParts[1] 118 + 119 + parser := jwt.Parser{ 120 + ValidMethods: []string{es256k.SigningMethodES256K.Alg()}, 121 + } 122 + 123 + token, err := parser.ParseWithClaims(accessToken, claims, func(token *jwt.Token) (interface{}, error) { 124 + if claims, ok := token.Claims.(*jwt.StandardClaims); ok { 125 + // Get the user's key from PLC Directory 126 + userDID := claims.Issuer 127 + entry, ok := auth.KeyCache.Get(userDID) 128 + if ok && entry.ExpiresAt.After(time.Now()) { 129 + cacheHits.WithLabelValues("key").Inc() 130 + span.SetAttributes(attribute.Bool("caches.keys.hit", true)) 131 + return entry.Key, nil 132 + } 133 + 134 + cacheMisses.WithLabelValues("key").Inc() 135 + span.SetAttributes(attribute.Bool("caches.keys.hit", false)) 136 + 137 + did, err := syntax.ParseDID(userDID) 138 + if err != nil { 139 + return nil, fmt.Errorf("Failed to parse user DID: %v", err) 140 + } 141 + 142 + // Get the user's key from PLC Directory 143 + id, err := auth.Dir.LookupDID(ctx, did) 144 + if err != nil { 145 + return nil, fmt.Errorf("Failed to lookup user DID: %v", err) 146 + } 147 + 148 + key, err := id.GetPublicKey("atproto") 149 + if err != nil { 150 + return nil, fmt.Errorf("Failed to get user public key: %v", err) 151 + } 152 + 153 + parsedPubkey, err := secec.NewPublicKey(key.UncompressedBytes()) 154 + if err != nil { 155 + return nil, fmt.Errorf("Failed to parse user public key: %v", err) 156 + } 157 + 158 + // Add the ECDSA key to the cache 159 + auth.KeyCache.Add(userDID, KeyCacheEntry{ 160 + Key: parsedPubkey, 161 + ExpiresAt: time.Now().Add(auth.KeyCacheTTL), 162 + }) 163 + 164 + return parsedPubkey, nil 165 + } 166 + 167 + return nil, fmt.Errorf("Invalid authorization token (failed to parse claims)") 168 + }) 169 + 170 + if err != nil { 171 + return fmt.Errorf("Failed to parse authorization token: %v", err) 172 + } 173 + 174 + if !token.Valid { 175 + return fmt.Errorf("Invalid authorization token") 176 + } 177 + 178 + return nil 179 + } 180 + 181 + func (auth *Auth) AuthenticateGinRequestViaJWT(c *gin.Context) { 182 + tracer := otel.Tracer("auth") 183 + ctx, span := tracer.Start(c.Request.Context(), "Auth:AuthenticateGinRequestViaJWT") 184 + 185 + authHeader := c.GetHeader("Authorization") 186 + if authHeader == "" { 187 + span.End() 188 + c.Next() 189 + return 190 + } 191 + 192 + claims := jwt.StandardClaims{} 193 + 194 + err := auth.GetClaimsFromAuthHeader(ctx, authHeader, &claims) 195 + if err != nil { 196 + c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Errorf("Failed to get claims from auth header: %v", err).Error()}) 197 + span.End() 198 + c.Abort() 199 + return 200 + } 201 + 202 + if claims.Audience != auth.ServiceDID { 203 + c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("Invalid audience (expected %s)", auth.ServiceDID)}) 204 + c.Abort() 205 + return 206 + } 207 + 208 + // Set claims Issuer to context as user DID 209 + c.Set("user_did", claims.Issuer) 210 + span.SetAttributes(attribute.String("user.did", claims.Issuer)) 211 + span.End() 212 + c.Next() 213 + }
+80
go.mod
··· 1 + module tangled.org/whey.party/rdcs 2 + 3 + go 1.25.4 4 + 5 + require ( 6 + github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc 7 + github.com/ericvolp12/jwt-go-secp256k1 v0.0.2 8 + github.com/gin-gonic/gin v1.11.0 9 + github.com/golang-jwt/jwt v3.2.2+incompatible 10 + github.com/google/uuid v1.6.0 11 + github.com/gorilla/websocket v1.5.3 12 + github.com/hashicorp/golang-lru/arc/v2 v2.0.7 13 + github.com/prometheus/client_golang v1.23.2 14 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b 15 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 16 + go.opentelemetry.io/otel v1.38.0 17 + golang.org/x/time v0.14.0 18 + ) 19 + 20 + require ( 21 + github.com/beorn7/perks v1.0.1 // indirect 22 + github.com/bytedance/sonic v1.14.0 // indirect 23 + github.com/bytedance/sonic/loader v0.3.0 // indirect 24 + github.com/cespare/xxhash/v2 v2.3.0 // indirect 25 + github.com/cloudwego/base64x v0.1.6 // indirect 26 + github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 27 + github.com/felixge/httpsnoop v1.0.4 // indirect 28 + github.com/gabriel-vasile/mimetype v1.4.8 // indirect 29 + github.com/gin-contrib/sse v1.1.0 // indirect 30 + github.com/go-logr/logr v1.4.3 // indirect 31 + github.com/go-logr/stdr v1.2.2 // indirect 32 + github.com/go-playground/locales v0.14.1 // indirect 33 + github.com/go-playground/universal-translator v0.18.1 // indirect 34 + github.com/go-playground/validator/v10 v10.27.0 // indirect 35 + github.com/goccy/go-json v0.10.2 // indirect 36 + github.com/goccy/go-yaml v1.18.0 // indirect 37 + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 38 + github.com/ipfs/go-cid v0.4.1 // indirect 39 + github.com/json-iterator/go v1.1.12 // indirect 40 + github.com/klauspost/cpuid/v2 v2.3.0 // indirect 41 + github.com/leodido/go-urn v1.4.0 // indirect 42 + github.com/mattn/go-isatty v0.0.20 // indirect 43 + github.com/minio/sha256-simd v1.0.1 // indirect 44 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 45 + github.com/modern-go/reflect2 v1.0.2 // indirect 46 + github.com/mr-tron/base58 v1.2.0 // indirect 47 + github.com/multiformats/go-base32 v0.1.0 // indirect 48 + github.com/multiformats/go-base36 v0.2.0 // indirect 49 + github.com/multiformats/go-multibase v0.2.0 // indirect 50 + github.com/multiformats/go-multihash v0.2.3 // indirect 51 + github.com/multiformats/go-varint v0.0.7 // indirect 52 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 53 + github.com/pelletier/go-toml/v2 v2.2.4 // indirect 54 + github.com/prometheus/client_model v0.6.2 // indirect 55 + github.com/prometheus/common v0.66.1 // indirect 56 + github.com/prometheus/procfs v0.16.1 // indirect 57 + github.com/quic-go/qpack v0.5.1 // indirect 58 + github.com/quic-go/quic-go v0.54.0 // indirect 59 + github.com/spaolacci/murmur3 v1.1.0 // indirect 60 + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 61 + github.com/ugorji/go/codec v1.3.0 // indirect 62 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 63 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 64 + go.opentelemetry.io/auto/sdk v1.1.0 // indirect 65 + go.opentelemetry.io/otel/metric v1.38.0 // indirect 66 + go.opentelemetry.io/otel/trace v1.38.0 // indirect 67 + go.uber.org/mock v0.5.0 // indirect 68 + go.yaml.in/yaml/v2 v2.4.2 // indirect 69 + golang.org/x/arch v0.20.0 // indirect 70 + golang.org/x/crypto v0.41.0 // indirect 71 + golang.org/x/mod v0.26.0 // indirect 72 + golang.org/x/net v0.43.0 // indirect 73 + golang.org/x/sync v0.16.0 // indirect 74 + golang.org/x/sys v0.35.0 // indirect 75 + golang.org/x/text v0.28.0 // indirect 76 + golang.org/x/tools v0.35.0 // indirect 77 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 78 + google.golang.org/protobuf v1.36.9 // indirect 79 + lukechampine.com/blake3 v1.2.1 // indirect 80 + )
+182
go.sum
··· 1 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 + github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc h1:2t+uAvfzJiCsTMwn5fW85t/IGa0+2I7BXS2ORastK4o= 4 + github.com/bluesky-social/indigo v0.0.0-20251202051123-81f317e322bc/go.mod h1:Pm2I1+iDXn/hLbF7XCg/DsZi6uDCiOo7hZGWprSM7k0= 5 + github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= 6 + github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= 7 + github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 8 + github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 9 + github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 + github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 + github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 12 + github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 13 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 + github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 17 + github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 18 + github.com/ericvolp12/jwt-go-secp256k1 v0.0.2 h1:puGwrNTY2vCt8eakkSEq2yeNxUD3zb2kPhv1OsF1hPs= 19 + github.com/ericvolp12/jwt-go-secp256k1 v0.0.2/go.mod h1:ntxzdN7EhBp8h+N78AtN2hjbVKHa7mijryYd9nPMyMo= 20 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 21 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 22 + github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 23 + github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 24 + github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 25 + github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 26 + github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 27 + github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 28 + github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 29 + github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 30 + github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 31 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 32 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 33 + github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 34 + github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 35 + github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 36 + github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 37 + github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 38 + github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 39 + github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 40 + github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 41 + github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 42 + github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 43 + github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 44 + github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 45 + github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 46 + github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 47 + github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 48 + github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 49 + github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 50 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 51 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 52 + github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 53 + github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 54 + github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= 55 + github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= 56 + github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 57 + github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 61 + github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 62 + github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 63 + github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 64 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 65 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 66 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 67 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 68 + github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 69 + github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 70 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 71 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 72 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 73 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 74 + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 75 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 76 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 77 + github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 78 + github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 79 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 80 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 81 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 82 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 83 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 84 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 85 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 86 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 87 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 88 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 89 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 90 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 91 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 92 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 93 + github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 94 + github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 95 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 96 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 97 + github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 98 + github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 99 + github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 100 + github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 101 + github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 102 + github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 103 + github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 104 + github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 105 + github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 106 + github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 107 + github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= 108 + github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 109 + github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 110 + github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 111 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 112 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 113 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 115 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 116 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 117 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 118 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 119 + github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 120 + github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 121 + github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 122 + github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 123 + github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 124 + github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 125 + github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 126 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 127 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 128 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 129 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 130 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 131 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 132 + go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 133 + go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 134 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= 135 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= 136 + go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= 137 + go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 138 + go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= 139 + go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= 140 + go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= 141 + go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= 142 + go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= 143 + go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 144 + go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 145 + go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 146 + go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 147 + go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 148 + go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 149 + go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 150 + go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 151 + go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 152 + golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= 153 + golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 154 + golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 155 + golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 156 + golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= 157 + golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 158 + golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 159 + golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 160 + golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 161 + golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 162 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 163 + golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 164 + golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 165 + golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 166 + golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 167 + golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 168 + golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 169 + golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= 170 + golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= 171 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 172 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 173 + google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= 174 + google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 175 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 176 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 177 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 178 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 180 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 181 + lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 182 + lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+67
main.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "net/http" 7 + "os" 8 + "time" 9 + 10 + "tangled.org/whey.party/rdcs/sticket" 11 + // "github.com/bluesky-social/indigo/atproto/atclient" 12 + // comatproto "github.com/bluesky-social/indigo/api/atproto" 13 + // appbsky "github.com/bluesky-social/indigo/api/bsky" 14 + // "github.com/bluesky-social/indigo/atproto/atclient" 15 + // "github.com/bluesky-social/indigo/atproto/identity" 16 + // "github.com/bluesky-social/indigo/atproto/syntax" 17 + // "github.com/bluesky-social/jetstream/pkg/models" 18 + ) 19 + 20 + const ( 21 + JETSTREAM_URL = "ws://localhost:6008/subscribe" 22 + SPACEDUST_URL = "ws://localhost:9998/subscribe" 23 + SLINGSHOT_URL = "http://localhost:7729" 24 + CONSTELLATION_URL = "http://localhost:7728" 25 + ) 26 + 27 + func main() { 28 + fmt.Fprintf(os.Stdout, "RDCS started") 29 + 30 + mailbox := sticket.New() 31 + 32 + http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 33 + mailbox.HandleWS(&w, r) 34 + }) 35 + 36 + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 37 + fmt.Fprintf(w, "hello worldio !") 38 + clientUUID := sticket.GetUUIDFromRequest(r) 39 + hasSticket := clientUUID != "" 40 + if hasSticket { 41 + go func(targetUUID string) { 42 + // simulated heavy processing 43 + time.Sleep(2 * time.Second) 44 + 45 + lateData := map[string]any{ 46 + "postId": 101, 47 + "newComments": []string{ 48 + "Wow great tutorial!", 49 + "I am stuck on step 1.", 50 + }, 51 + } 52 + 53 + success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData) 54 + if success { 55 + log.Println("Successfully sent late data via Sticket") 56 + } else { 57 + log.Println("Failed to send late data (client disconnected?)") 58 + } 59 + }(clientUUID) 60 + } 61 + }) 62 + http.ListenAndServe(":7152", nil) 63 + } 64 + 65 + func getPostThreadV2(w http.ResponseWriter, r *http.Request) { 66 + fmt.Fprintf(w, "hello worldio !") 67 + }
+130
sticket/sticket.go
··· 1 + package sticket 2 + 3 + import ( 4 + "log" 5 + "net/http" 6 + "sync" 7 + 8 + "github.com/google/uuid" 9 + "github.com/gorilla/websocket" 10 + ) 11 + 12 + const HeaderKey = "X-STICKET" 13 + 14 + type Event struct { 15 + Type string `json:"type"` 16 + Payload any `json:"payload"` 17 + } 18 + 19 + type Manager struct { 20 + clients map[string]*websocket.Conn 21 + mu sync.RWMutex 22 + upgrader websocket.Upgrader 23 + } 24 + 25 + func New() *Manager { 26 + return &Manager{ 27 + clients: make(map[string]*websocket.Conn), 28 + upgrader: websocket.Upgrader{ 29 + CheckOrigin: func(r *http.Request) bool { return true }, 30 + }, 31 + } 32 + } 33 + 34 + func (m *Manager) HandleWS(w *http.ResponseWriter, r *http.Request) { 35 + conn, err := m.upgrader.Upgrade(*w, r, nil) 36 + if err != nil { 37 + log.Printf("Sticket: Failed to upgrade: %v", err) 38 + return 39 + } 40 + 41 + clientUUID := r.URL.Query().Get("uuid") 42 + //isNew := false 43 + 44 + if clientUUID == "" { 45 + //isNew = true 46 + clientUUID = uuid.New().String() 47 + } else { 48 + if _, err := uuid.Parse(clientUUID); err != nil { 49 + //isNew = true 50 + clientUUID = uuid.New().String() 51 + } 52 + } 53 + 54 + m.register(clientUUID, conn) 55 + 56 + defer m.unregister(clientUUID) 57 + 58 + initEvent := Event{ 59 + Type: "init", 60 + Payload: map[string]string{"uuid": clientUUID}, 61 + } 62 + if err := m.sendJson(conn, initEvent); err != nil { 63 + return 64 + } 65 + 66 + for { 67 + if _, _, err := conn.NextReader(); err != nil { 68 + break 69 + } 70 + } 71 + } 72 + 73 + func (m *Manager) SendToClient(uuid string, eventType string, data any) bool { 74 + m.mu.RLock() 75 + conn, ok := m.clients[uuid] 76 + m.mu.RUnlock() 77 + 78 + if !ok { 79 + return false 80 + } 81 + 82 + evt := Event{ 83 + Type: eventType, 84 + Payload: data, 85 + } 86 + 87 + m.mu.Lock() 88 + defer m.mu.Unlock() 89 + 90 + if conn, ok = m.clients[uuid]; !ok { 91 + return false 92 + } 93 + 94 + if err := m.sendJson(conn, evt); err != nil { 95 + conn.Close() 96 + delete(m.clients, uuid) 97 + return false 98 + } 99 + 100 + return true 101 + } 102 + 103 + func GetUUIDFromRequest(r *http.Request) string { 104 + return r.Header.Get(HeaderKey) 105 + } 106 + 107 + func (m *Manager) register(id string, conn *websocket.Conn) { 108 + m.mu.Lock() 109 + defer m.mu.Unlock() 110 + 111 + if existing, ok := m.clients[id]; ok { 112 + existing.Close() 113 + } 114 + m.clients[id] = conn 115 + log.Printf("Sticket: Client connected [%s]", id) 116 + } 117 + 118 + func (m *Manager) unregister(id string) { 119 + m.mu.Lock() 120 + defer m.mu.Unlock() 121 + if conn, ok := m.clients[id]; ok { 122 + conn.Close() 123 + delete(m.clients, id) 124 + log.Printf("Sticket: Client disconnected [%s]", id) 125 + } 126 + } 127 + 128 + func (m *Manager) sendJson(conn *websocket.Conn, v any) error { 129 + return conn.WriteJSON(v) 130 + }