this repo has no description
at main 3.4 kB view raw
1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "log" 9 "log/slog" 10 "net/http" 11 "os" 12 "os/signal" 13 "path" 14 "syscall" 15 16 tangledalertbot "tangled.sh/willdot.net/tangled-alert-bot" 17 18 "github.com/avast/retry-go/v4" 19 "github.com/bugsnag/bugsnag-go" 20 "github.com/joho/godotenv" 21) 22 23const ( 24 defaultJetstreamAddr = "wss://jetstream.atproto.tools/subscribe" 25) 26 27func main() { 28 err := run() 29 if err != nil { 30 log.Fatal(err) 31 } 32} 33 34func run() error { 35 err := godotenv.Load() 36 if err != nil && !os.IsNotExist(err) { 37 return fmt.Errorf("error loading .env file") 38 } 39 40 signals := make(chan os.Signal, 1) 41 signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) 42 43 bugsnag.Configure(bugsnag.Configuration{ 44 APIKey: os.Getenv("BUGSNAG"), 45 }) 46 47 dbPath := os.Getenv("DATABASE_PATH") 48 if dbPath == "" { 49 dbPath = "./" 50 } 51 52 dbFilename := path.Join(dbPath, "database.db") 53 database, err := tangledalertbot.NewDatabase(dbFilename) 54 if err != nil { 55 return fmt.Errorf("create new store: %w", err) 56 } 57 defer database.Close() 58 59 ctx, cancel := context.WithCancel(context.Background()) 60 defer cancel() 61 62 go consumeLoop(ctx, database) 63 64 go startHttpServer(ctx, database) 65 66 <-signals 67 cancel() 68 69 return nil 70} 71 72func consumeLoop(ctx context.Context, database *tangledalertbot.Database) { 73 handler := tangledalertbot.NewFeedHandler(database) 74 75 jsServerAddr := os.Getenv("JS_SERVER_ADDR") 76 if jsServerAddr == "" { 77 jsServerAddr = defaultJetstreamAddr 78 } 79 80 consumer := tangledalertbot.NewJetstreamConsumer(jsServerAddr, slog.Default(), handler) 81 82 _ = retry.Do(func() error { 83 err := consumer.Consume(ctx) 84 if err != nil { 85 // if the context has been cancelled then it's time to exit 86 if errors.Is(err, context.Canceled) { 87 return nil 88 } 89 return err 90 } 91 return nil 92 }, retry.Attempts(0)) // retry indefinitly until context canceled 93 94 slog.Warn("exiting consume loop") 95} 96 97func startHttpServer(ctx context.Context, db *tangledalertbot.Database) { 98 srv := server{ 99 db: db, 100 } 101 mux := http.NewServeMux() 102 mux.HandleFunc("/issues", srv.handleListIssues) 103 mux.HandleFunc("/comments", srv.handleListComments) 104 105 err := http.ListenAndServe(":3000", mux) 106 if err != nil { 107 slog.Error("http listen and serve", "error", err) 108 } 109} 110 111type server struct { 112 db *tangledalertbot.Database 113} 114 115func (s *server) handleListIssues(w http.ResponseWriter, r *http.Request) { 116 issues, err := s.db.GetIssues() 117 if err != nil { 118 slog.Error("getting issues from DB", "error", err) 119 http.Error(w, "error getting issues from DB", http.StatusInternalServerError) 120 return 121 } 122 123 b, err := json.Marshal(issues) 124 if err != nil { 125 slog.Error("marshalling issues from DB", "error", err) 126 http.Error(w, "marshalling issues from DB", http.StatusInternalServerError) 127 return 128 } 129 130 w.Header().Set("Content-Type", "application/json") 131 w.Write(b) 132} 133 134func (s *server) handleListComments(w http.ResponseWriter, r *http.Request) { 135 comments, err := s.db.GetComments() 136 if err != nil { 137 slog.Error("getting comments from DB", "error", err) 138 http.Error(w, "error getting comments from DB", http.StatusInternalServerError) 139 return 140 } 141 142 b, err := json.Marshal(comments) 143 if err != nil { 144 slog.Error("marshalling comments from DB", "error", err) 145 http.Error(w, "marshalling comments from DB", http.StatusInternalServerError) 146 return 147 } 148 149 w.Header().Set("Content-Type", "application/json") 150 w.Write(b) 151}