package main import ( "context" "encoding/json" "errors" "fmt" "log" "log/slog" "net/http" "os" "os/signal" "path" "syscall" tangledalertbot "tangled.sh/willdot.net/tangled-alert-bot" "github.com/avast/retry-go/v4" "github.com/bugsnag/bugsnag-go" "github.com/joho/godotenv" ) const ( defaultJetstreamAddr = "wss://jetstream.atproto.tools/subscribe" ) func main() { err := run() if err != nil { log.Fatal(err) } } func run() error { err := godotenv.Load() if err != nil && !os.IsNotExist(err) { return fmt.Errorf("error loading .env file") } signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) bugsnag.Configure(bugsnag.Configuration{ APIKey: os.Getenv("BUGSNAG"), }) dbPath := os.Getenv("DATABASE_PATH") if dbPath == "" { dbPath = "./" } dbFilename := path.Join(dbPath, "database.db") database, err := tangledalertbot.NewDatabase(dbFilename) if err != nil { return fmt.Errorf("create new store: %w", err) } defer database.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() go consumeLoop(ctx, database) go startHttpServer(ctx, database) <-signals cancel() return nil } func consumeLoop(ctx context.Context, database *tangledalertbot.Database) { handler := tangledalertbot.NewFeedHandler(database) jsServerAddr := os.Getenv("JS_SERVER_ADDR") if jsServerAddr == "" { jsServerAddr = defaultJetstreamAddr } consumer := tangledalertbot.NewJetstreamConsumer(jsServerAddr, slog.Default(), handler) _ = retry.Do(func() error { err := consumer.Consume(ctx) if err != nil { // if the context has been cancelled then it's time to exit if errors.Is(err, context.Canceled) { return nil } return err } return nil }, retry.Attempts(0)) // retry indefinitly until context canceled slog.Warn("exiting consume loop") } func startHttpServer(ctx context.Context, db *tangledalertbot.Database) { srv := server{ db: db, } mux := http.NewServeMux() mux.HandleFunc("/issues", srv.handleListIssues) mux.HandleFunc("/comments", srv.handleListComments) err := http.ListenAndServe(":3000", mux) if err != nil { slog.Error("http listen and serve", "error", err) } } type server struct { db *tangledalertbot.Database } func (s *server) handleListIssues(w http.ResponseWriter, r *http.Request) { issues, err := s.db.GetIssues() if err != nil { slog.Error("getting issues from DB", "error", err) http.Error(w, "error getting issues from DB", http.StatusInternalServerError) return } b, err := json.Marshal(issues) if err != nil { slog.Error("marshalling issues from DB", "error", err) http.Error(w, "marshalling issues from DB", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(b) } func (s *server) handleListComments(w http.ResponseWriter, r *http.Request) { comments, err := s.db.GetComments() if err != nil { slog.Error("getting comments from DB", "error", err) http.Error(w, "error getting comments from DB", http.StatusInternalServerError) return } b, err := json.Marshal(comments) if err != nil { slog.Error("marshalling comments from DB", "error", err) http.Error(w, "marshalling comments from DB", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(b) }