A collection of Custom Bluesky Feeds, including Fresh Feeds, all under one roof
at main 10 kB view raw
1package main 2 3import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "log" 8 "net/http" 9 "net/url" 10 "os" 11 "time" 12 13 _ "github.com/lib/pq" 14 15 auth "github.com/ericvolp12/go-bsky-feed-generator/pkg/auth" 16 "github.com/ericvolp12/go-bsky-feed-generator/pkg/feedrouter" 17 ginendpoints "github.com/ericvolp12/go-bsky-feed-generator/pkg/gin" 18 19 freshfeeds "github.com/ericvolp12/go-bsky-feed-generator/pkg/feeds/fresh" 20 staticfeed "github.com/ericvolp12/go-bsky-feed-generator/pkg/feeds/static" 21 ginprometheus "github.com/ericvolp12/go-gin-prometheus" 22 "github.com/gin-gonic/gin" 23 "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" 24 "go.opentelemetry.io/otel" 25 "go.opentelemetry.io/otel/exporters/otlp/otlptrace" 26 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 27 "go.opentelemetry.io/otel/sdk/resource" 28 sdktrace "go.opentelemetry.io/otel/sdk/trace" 29 semconv "go.opentelemetry.io/otel/semconv/v1.17.0" 30) 31 32func main() { 33 ctx := context.Background() 34 35 // Open the database connection 36 dbHost := os.Getenv("DB_HOST") 37 dbUser := os.Getenv("DB_USER") 38 dbName := os.Getenv("DB_NAME") 39 dbPassword := os.Getenv("DB_PASSWORD") 40 db, err := sql.Open("postgres", fmt.Sprintf("user=%s dbname=%s host=%s password=%s sslmode=disable", dbUser, dbName, dbHost, dbPassword)) 41 if err != nil { 42 log.Fatalf("Failed to open database: %v", err) 43 } 44 defer db.Close() 45 46 // Ping the database to ensure the connection is established 47 if err := db.Ping(); err != nil { 48 log.Fatalf("Failed to ping database: %v", err) 49 } 50 51 // Configure feed generator from environment variables 52 53 // Registers a tracer Provider globally if the exporter endpoint is set 54 if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" { 55 log.Println("initializing tracer...") 56 shutdown, err := installExportPipeline(ctx) 57 if err != nil { 58 log.Fatal(err) 59 } 60 defer func() { 61 if err := shutdown(ctx); err != nil { 62 log.Fatal(err) 63 } 64 }() 65 } 66 67 feedActorDID := os.Getenv("FEED_ACTOR_DID") 68 if feedActorDID == "" { 69 log.Fatal("FEED_ACTOR_DID environment variable must be set") 70 } 71 72 // serviceEndpoint is a URL that the feed generator will be available at 73 serviceEndpoint := os.Getenv("SERVICE_ENDPOINT") 74 if serviceEndpoint == "" { 75 log.Fatal("SERVICE_ENDPOINT environment variable must be set") 76 } 77 78 // Set the acceptable DIDs for the feed generator to respond to 79 // We'll default to the feedActorDID and the Service Endpoint as a did:web 80 serviceURL, err := url.Parse(serviceEndpoint) 81 if err != nil { 82 log.Fatal(fmt.Errorf("error parsing service endpoint: %w", err)) 83 } 84 85 serviceWebDID := "did:web:" + serviceURL.Hostname() 86 87 log.Printf("service DID Web: %s", serviceWebDID) 88 89 acceptableDIDs := []string{feedActorDID, serviceWebDID} 90 91 // Create a new feed router instance 92 feedRouter, err := feedrouter.NewFeedRouter(ctx, feedActorDID, serviceWebDID, acceptableDIDs, serviceEndpoint) 93 if err != nil { 94 log.Fatal(fmt.Errorf("error creating feed router: %w", err)) 95 } 96 97 // Here we can add feeds to the Feed Router instance 98 // Feeds conform to the Feed interface, which is defined in 99 // pkg/feedrouter/feedrouter.go 100 101 // For demonstration purposes, we'll use a static feed generator 102 // that will always return the same feed skeleton (one post) 103 staticFeed, staticFeedAliases, err := staticfeed.NewStaticFeed( 104 ctx, 105 feedActorDID, 106 "static", 107 // This static post is the conversation that sparked this demo repo 108 []string{"at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.post/3jx7msc4ive26"}, 109 ) 110 111 // idk help me 112 113 rindsFeed, rindsFeedAliases, err := freshfeeds.NewStaticFeed( 114 ctx, 115 feedActorDID, 116 "rinds", 117 // This static post is the conversation that sparked this demo repo 118 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 119 db, 120 "rinds", 121 false, 122 ) 123 124 localrindsFeed, localrindsFeedAliases, err := freshfeeds.NewStaticFeed( 125 ctx, 126 feedActorDID, 127 "localrinds-test", 128 // This static post is the conversation that sparked this demo repo 129 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 130 db, 131 "localrinds-test", 132 false, 133 ) 134 135 randomFeed, randomFeedAliases, err := freshfeeds.NewStaticFeed( 136 ctx, 137 feedActorDID, 138 "random", 139 // This static post is the conversation that sparked this demo repo 140 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 141 db, 142 "random", 143 false, 144 ) 145 146 repostsFeed, repostsFeedAliases, err := freshfeeds.NewStaticFeed( 147 ctx, 148 feedActorDID, 149 "reposts", 150 // This static post is the conversation that sparked this demo repo 151 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 152 db, 153 "reposts", 154 false, 155 ) 156 mnineFeed, mnineFeedAliases, err := freshfeeds.NewStaticFeed( 157 ctx, 158 feedActorDID, 159 "mnine", 160 // This static post is the conversation that sparked this demo repo 161 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 162 db, 163 "mnine", 164 false, 165 ) 166 167 rrindsFeed, rrindsFeedAliases, err := freshfeeds.NewStaticFeed( 168 ctx, 169 feedActorDID, 170 "rinds-replies", 171 // This static post is the conversation that sparked this demo repo 172 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 173 db, 174 "rinds", 175 true, 176 ) 177 178 rrandomFeed, rrandomFeedAliases, err := freshfeeds.NewStaticFeed( 179 ctx, 180 feedActorDID, 181 "random-replies", 182 // This static post is the conversation that sparked this demo repo 183 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 184 db, 185 "random", 186 true, 187 ) 188 189 rrepostsFeed, rrepostsFeedAliases, err := freshfeeds.NewStaticFeed( 190 ctx, 191 feedActorDID, 192 "reposts-replies", 193 // This static post is the conversation that sparked this demo repo 194 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 195 db, 196 "reposts", 197 true, 198 ) 199 rmnineFeed, rmnineFeedAliases, err := freshfeeds.NewStaticFeed( 200 ctx, 201 feedActorDID, 202 "mnine-replies", 203 // This static post is the conversation that sparked this demo repo 204 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 205 db, 206 "mnine", 207 true, 208 ) 209 orepliesFeed, orepliesFeedAliases, err := freshfeeds.NewStaticFeed( 210 ctx, 211 feedActorDID, 212 "oreplies", 213 // This static post is the conversation that sparked this demo repo 214 []string{"at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3kgjjhlsnoi2f"}, 215 db, 216 "oreplies", 217 true, 218 ) 219 // Add the static feed to the feed generator 220 feedRouter.AddFeed(staticFeedAliases, staticFeed) 221 222 feedRouter.AddFeed(localrindsFeedAliases, localrindsFeed) 223 224 feedRouter.AddFeed(rindsFeedAliases, rindsFeed) 225 feedRouter.AddFeed(randomFeedAliases, randomFeed) 226 feedRouter.AddFeed(repostsFeedAliases, repostsFeed) 227 feedRouter.AddFeed(mnineFeedAliases, mnineFeed) 228 229 feedRouter.AddFeed(rrindsFeedAliases, rrindsFeed) 230 feedRouter.AddFeed(rrandomFeedAliases, rrandomFeed) 231 feedRouter.AddFeed(rrepostsFeedAliases, rrepostsFeed) 232 feedRouter.AddFeed(rmnineFeedAliases, rmnineFeed) 233 234 feedRouter.AddFeed(orepliesFeedAliases, orepliesFeed) 235 236 // Create a gin router with default middleware for logging and recovery 237 router := gin.Default() 238 239 // Plug in OTEL Middleware and skip metrics endpoint 240 router.Use( 241 otelgin.Middleware( 242 "go-bsky-feed-generator", 243 otelgin.WithFilter(func(req *http.Request) bool { 244 return req.URL.Path != "/metrics" 245 }), 246 ), 247 ) 248 249 // Add Prometheus metrics middleware 250 p := ginprometheus.NewPrometheus("gin", nil) 251 p.Use(router) 252 253 // Add unauthenticated routes for feed generator 254 ep := ginendpoints.NewEndpoints(feedRouter) 255 router.GET("/.well-known/did.json", ep.GetWellKnownDID) 256 router.GET("/xrpc/app.bsky.feed.describeFeedGenerator", ep.DescribeFeeds) 257 // Root route: ASCII art and GitHub link 258 router.GET("/", func(c *gin.Context) { 259 c.Header("Content-Type", "text/plain; charset=utf-8") 260 c.String(http.StatusOK, ` ____ _ _ 261 | _ \(_)_ __ __| |___ 262 | |_) | | '_ \ / _' / __| 263 | _ <| | | | | (_| \__ \ 264 |_| \_\_|_| |_|\__,_|___/ 265 266bsky feed generators by @whey.party 267 268Code: https://github.com/rimar1337/rinds 269Flagship Fresh Feeds Instance: https://bsky.app/profile/did:plc:mn45tewwnse5btfftvd3powc/feed/rinds 270 271Fresh Feeds icons by @pprmint.de 272Repository generated from https://github.com/ericvolp12/go-bsky-feed-generator 273`) 274 }) 275 276 // Plug in Authentication Middleware 277 auther, err := auth.NewAuth( 278 100_000, 279 time.Hour*12, 280 5, 281 serviceWebDID, 282 ) 283 if err != nil { 284 log.Fatalf("Failed to create Auth: %v", err) 285 } 286 287 router.Use(auther.AuthenticateGinRequestViaJWT) 288 289 // Add authenticated routes for feed generator 290 router.GET("/xrpc/app.bsky.feed.getFeedSkeleton", ep.GetFeedSkeleton) 291 292 port := os.Getenv("PORT") 293 if port == "" { 294 port = "8080" 295 } 296 297 log.Printf("Starting server on port %s", port) 298 router.Run(fmt.Sprintf(":%s", port)) 299} 300 301// installExportPipeline registers a trace provider instance as a global trace provider, 302func installExportPipeline(ctx context.Context) (func(context.Context) error, error) { 303 client := otlptracehttp.NewClient() 304 exporter, err := otlptrace.New(ctx, client) 305 if err != nil { 306 return nil, fmt.Errorf("creating OTLP trace exporter: %w", err) 307 } 308 309 tracerProvider := newTraceProvider(exporter) 310 otel.SetTracerProvider(tracerProvider) 311 312 return tracerProvider.Shutdown, nil 313} 314 315// newTraceProvider creates a new trace provider instance. 316func newTraceProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider { 317 // Ensure default SDK resources and the required service name are set. 318 r, err := resource.Merge( 319 resource.Default(), 320 resource.NewWithAttributes( 321 semconv.SchemaURL, 322 semconv.ServiceName("go-bsky-feed-generator"), 323 ), 324 ) 325 326 if err != nil { 327 panic(err) 328 } 329 330 // initialize the traceIDRatioBasedSampler to sample all traces 331 traceIDRatioBasedSampler := sdktrace.TraceIDRatioBased(1) 332 333 return sdktrace.NewTracerProvider( 334 sdktrace.WithSampler(traceIDRatioBasedSampler), 335 sdktrace.WithBatcher(exp), 336 sdktrace.WithResource(r), 337 ) 338}