A collection of Custom Bluesky Feeds, including Fresh Feeds, all under one roof
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}