this repo has no description
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}