bluesky appview implementation using microcosm and other services server.reddwarf.app
appview bluesky reddwarf microcosm

sticket init

Changed files
+672
auth
sticket
+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 + }