this repo has no description

update issues and comments in DB if they get updated on tangled. add http server to get data to test

authored by willdot.net and committed by

Tangled 538483ac 21295368

+139 -8
+1
Dockerfile
··· 6 6 RUN go mod download 7 7 COPY . . 8 8 9 + #compiling for Pi at the moment 9 10 RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -a -installsuffix cgo -o tangled-alert-bot ./cmd/. 10 11 11 12 FROM alpine:latest
+66
cmd/main.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "encoding/json" 5 6 "errors" 6 7 "fmt" 7 8 "log" 8 9 "log/slog" 10 + "net/http" 9 11 "os" 10 12 "os/signal" 11 13 "path" ··· 14 16 tangledalertbot "tangled.sh/willdot.net/tangled-alert-bot" 15 17 16 18 "github.com/avast/retry-go/v4" 19 + "github.com/bugsnag/bugsnag-go" 17 20 "github.com/joho/godotenv" 18 21 ) 19 22 ··· 37 40 signals := make(chan os.Signal, 1) 38 41 signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) 39 42 43 + bugsnag.Configure(bugsnag.Configuration{ 44 + APIKey: os.Getenv("BUGSNAG"), 45 + }) 46 + 40 47 dbPath := os.Getenv("DATABASE_PATH") 41 48 if dbPath == "" { 42 49 dbPath = "./" ··· 53 60 defer cancel() 54 61 55 62 go consumeLoop(ctx, database) 63 + 64 + go startHttpServer(ctx, database) 56 65 57 66 <-signals 58 67 cancel() ··· 78 87 return nil 79 88 } 80 89 slog.Error("consume loop", "error", err) 90 + bugsnag.Notify(err) 81 91 return err 82 92 } 83 93 return nil ··· 85 95 86 96 slog.Warn("exiting consume loop") 87 97 } 98 + 99 + func startHttpServer(ctx context.Context, db *tangledalertbot.Database) { 100 + srv := server{ 101 + db: db, 102 + } 103 + mux := http.NewServeMux() 104 + mux.HandleFunc("/issues", srv.handleListIssues) 105 + mux.HandleFunc("/comments", srv.handleListComments) 106 + 107 + err := http.ListenAndServe(":3000", mux) 108 + if err != nil { 109 + slog.Error("http listen and serve", "error", err) 110 + } 111 + } 112 + 113 + type server struct { 114 + db *tangledalertbot.Database 115 + } 116 + 117 + func (s *server) handleListIssues(w http.ResponseWriter, r *http.Request) { 118 + issues, err := s.db.GetIssues() 119 + if err != nil { 120 + slog.Error("getting issues from DB", "error", err) 121 + http.Error(w, "error getting issues from DB", http.StatusInternalServerError) 122 + return 123 + } 124 + 125 + b, err := json.Marshal(issues) 126 + if err != nil { 127 + slog.Error("marshalling issues from DB", "error", err) 128 + http.Error(w, "marshalling issues from DB", http.StatusInternalServerError) 129 + return 130 + } 131 + 132 + w.Header().Set("Content-Type", "application/json") 133 + w.Write(b) 134 + } 135 + 136 + func (s *server) handleListComments(w http.ResponseWriter, r *http.Request) { 137 + comments, err := s.db.GetComments() 138 + if err != nil { 139 + slog.Error("getting comments from DB", "error", err) 140 + http.Error(w, "error getting comments from DB", http.StatusInternalServerError) 141 + return 142 + } 143 + 144 + b, err := json.Marshal(comments) 145 + if err != nil { 146 + slog.Error("marshalling comments from DB", "error", err) 147 + http.Error(w, "marshalling comments from DB", http.StatusInternalServerError) 148 + return 149 + } 150 + 151 + w.Header().Set("Content-Type", "application/json") 152 + w.Write(b) 153 + }
+10 -3
consumer.go
··· 11 11 "github.com/bluesky-social/jetstream/pkg/client" 12 12 "github.com/bluesky-social/jetstream/pkg/client/schedulers/sequential" 13 13 "github.com/bluesky-social/jetstream/pkg/models" 14 + "github.com/bugsnag/bugsnag-go" 14 15 "tangled.sh/tangled.sh/core/api/tangled" 15 16 ) 16 17 ··· 100 101 } 101 102 102 103 switch event.Commit.Operation { 103 - case models.CommitOperationCreate: 104 + case models.CommitOperationCreate, models.CommitOperationUpdate: 104 105 return h.handleCreateEvent(ctx, event) 105 - // TODO: handle deletes too 106 + // TODO: handle deletes too 106 107 default: 107 108 return nil 108 109 } ··· 127 128 128 129 err := json.Unmarshal(event.Commit.Record, &issue) 129 130 if err != nil { 131 + bugsnag.Notify(err) 130 132 slog.Error("error unmarshalling event record to issue", "error", err) 131 133 return 132 134 } ··· 136 138 137 139 createdAt, err := time.Parse(time.RFC3339, issue.CreatedAt) 138 140 if err != nil { 141 + bugsnag.Notify(err) 139 142 slog.Error("parsing createdAt time from issue", "error", err, "timestamp", issue.CreatedAt) 140 143 createdAt = time.Now().UTC() 141 144 } 142 145 body := "" 143 146 if issue.Body != nil { 144 - body = *&body 147 + body = *issue.Body 145 148 } 146 149 err = h.store.CreateIssue(Issue{ 147 150 AuthorDID: did, ··· 152 155 Repo: issue.Repo, 153 156 }) 154 157 if err != nil { 158 + bugsnag.Notify(err) 155 159 slog.Error("create issue", "error", err, "did", did, "rkey", rkey) 156 160 return 157 161 } ··· 163 167 164 168 err := json.Unmarshal(event.Commit.Record, &comment) 165 169 if err != nil { 170 + bugsnag.Notify(err) 166 171 slog.Error("error unmarshalling event record to comment", "error", err) 167 172 return 168 173 } ··· 172 177 173 178 createdAt, err := time.Parse(time.RFC3339, comment.CreatedAt) 174 179 if err != nil { 180 + bugsnag.Notify(err) 175 181 slog.Error("parsing createdAt time from comment", "error", err, "timestamp", comment.CreatedAt) 176 182 createdAt = time.Now().UTC() 177 183 } ··· 184 190 //ReplyTo: comment, // TODO: there should be a ReplyTo field that can be used as well once the right type is imported 185 191 }) 186 192 if err != nil { 193 + bugsnag.Notify(err) 187 194 slog.Error("create comment", "error", err, "did", did, "rkey", rkey) 188 195 return 189 196 }
+42 -2
database.go
··· 125 125 126 126 // CreateIssue will insert a issue into a database 127 127 func (d *Database) CreateIssue(issue Issue) error { 128 - sql := `INSERT INTO issues (authorDid, rkey, title, body, repo, createdAt) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(authorDid, rkey) DO NOTHING;` 128 + sql := `REPLACE INTO issues (authorDid, rkey, title, body, repo, createdAt) VALUES (?, ?, ?, ?, ?, ?);` 129 129 _, err := d.db.Exec(sql, issue.AuthorDID, issue.RKey, issue.Title, issue.Body, issue.Repo, issue.CreatedAt) 130 130 if err != nil { 131 131 return fmt.Errorf("exec insert issue: %w", err) ··· 135 135 136 136 // CreateComment will insert a comment into a database 137 137 func (d *Database) CreateComment(comment Comment) error { 138 - sql := `INSERT INTO comments (authorDid, rkey, body, issue, replyTo, createdAt) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(authorDid, rkey) DO NOTHING;` 138 + sql := `REPLACE INTO comments (authorDid, rkey, body, issue, replyTo, createdAt) VALUES (?, ?, ?, ?, ?, ?);` 139 139 _, err := d.db.Exec(sql, comment.AuthorDID, comment.RKey, comment.Body, comment.Issue, comment.ReplyTo, comment.CreatedAt) 140 140 if err != nil { 141 141 return fmt.Errorf("exec insert comment: %w", err) 142 142 } 143 143 return nil 144 144 } 145 + 146 + func (d *Database) GetIssues() ([]Issue, error) { 147 + sql := "SELECT authorDid, rkey, title, body, repo, createdAt FROM issues;" 148 + rows, err := d.db.Query(sql) 149 + if err != nil { 150 + return nil, fmt.Errorf("run query to get issues: %w", err) 151 + } 152 + defer rows.Close() 153 + 154 + var results []Issue 155 + for rows.Next() { 156 + var issue Issue 157 + if err := rows.Scan(&issue.AuthorDID, &issue.RKey, &issue.Title, &issue.Body, &issue.Repo, &issue.CreatedAt); err != nil { 158 + return nil, fmt.Errorf("scan row: %w", err) 159 + } 160 + 161 + results = append(results, issue) 162 + } 163 + return results, nil 164 + } 165 + 166 + func (d *Database) GetComments() ([]Comment, error) { 167 + sql := "SELECT authorDid, rkey, body, issue, replyTo, createdAt FROM comments;" 168 + rows, err := d.db.Query(sql) 169 + if err != nil { 170 + return nil, fmt.Errorf("run query to get comments: %w", err) 171 + } 172 + defer rows.Close() 173 + 174 + var results []Comment 175 + for rows.Next() { 176 + var comment Comment 177 + if err := rows.Scan(&comment.AuthorDID, &comment.RKey, &comment.Body, &comment.Issue, &comment.ReplyTo, &comment.CreatedAt); err != nil { 178 + return nil, fmt.Errorf("scan row: %w", err) 179 + } 180 + 181 + results = append(results, comment) 182 + } 183 + return results, nil 184 + }
+2
docker-compose.yaml
··· 2 2 tangled-alert-bot: 3 3 container_name: tangled-alert-bot 4 4 image: willdot/tangled-alert-bot 5 + ports: 6 + - "3000:3000" 5 7 volumes: 6 8 - ./data:/app/data 7 9 environment:
+6 -1
go.mod
··· 5 5 require ( 6 6 github.com/avast/retry-go/v4 v4.6.1 7 7 github.com/bluesky-social/jetstream v0.0.0-20250815235753-306e46369336 8 + github.com/bugsnag/bugsnag-go v2.6.2+incompatible 8 9 github.com/glebarez/go-sqlite v1.22.0 9 10 github.com/joho/godotenv v1.5.1 10 - tangled.sh/tangled.sh/core v1.8.1-alpha 11 + tangled.sh/tangled.sh/core v1.8.1-alpha.0.20250828210137-07b009bd6b98 11 12 ) 12 13 13 14 require ( 14 15 github.com/beorn7/perks v1.0.1 // indirect 16 + github.com/bitly/go-simplejson v0.5.1 // indirect 15 17 github.com/bluesky-social/indigo v0.0.0-20250808182429-6f0837c2d12b // indirect 18 + github.com/bugsnag/panicwrap v1.3.4 // indirect 16 19 github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 20 github.com/dustin/go-humanize v1.0.1 // indirect 18 21 github.com/goccy/go-json v0.10.5 // indirect 19 22 github.com/google/uuid v1.6.0 // indirect 20 23 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 21 24 github.com/ipfs/go-cid v0.5.0 // indirect 25 + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 22 26 github.com/klauspost/compress v1.18.0 // indirect 23 27 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 24 28 github.com/mattn/go-isatty v0.0.20 // indirect ··· 30 34 github.com/multiformats/go-multihash v0.2.3 // indirect 31 35 github.com/multiformats/go-varint v0.0.7 // indirect 32 36 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 37 + github.com/pkg/errors v0.9.1 // indirect 33 38 github.com/prometheus/client_golang v1.22.0 // indirect 34 39 github.com/prometheus/client_model v0.6.2 // indirect 35 40 github.com/prometheus/common v0.64.0 // indirect
+12 -2
go.sum
··· 2 2 github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= 3 3 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 4 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 + github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= 6 + github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= 5 7 github.com/bluesky-social/indigo v0.0.0-20250808182429-6f0837c2d12b h1:bJTlFwMhB9sluuqZxVXtv2yFcaWOC/PZokz9mcwb4u4= 6 8 github.com/bluesky-social/indigo v0.0.0-20250808182429-6f0837c2d12b/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q= 7 9 github.com/bluesky-social/jetstream v0.0.0-20250815235753-306e46369336 h1:NM3wfeFUrdjCE/xHLXQorwQvEKlI9uqnWl7L0Y9KA8U= 8 10 github.com/bluesky-social/jetstream v0.0.0-20250815235753-306e46369336/go.mod h1:3ihWQCbXeayg41G8lQ5DfB/3NnEhl0XX24eZ3mLpf7Q= 11 + github.com/bugsnag/bugsnag-go v2.6.2+incompatible h1:6R/uadVvhrciRbPXFmCY7sZ7ElbGKsxxOvG78HcGwj8= 12 + github.com/bugsnag/bugsnag-go v2.6.2+incompatible/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= 13 + github.com/bugsnag/panicwrap v1.3.4 h1:A6sXFtDGsgU/4BLf5JT0o5uYg3EeKgGx3Sfs+/uk3pU= 14 + github.com/bugsnag/panicwrap v1.3.4/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= 9 15 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 16 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 17 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= ··· 28 34 github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 29 35 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 30 36 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 37 + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= 38 + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 31 39 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 32 40 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 33 41 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= ··· 50 58 github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 51 59 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 52 60 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 61 + github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 62 + github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 53 63 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 54 64 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 65 github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= ··· 93 103 modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= 94 104 modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= 95 105 modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= 96 - tangled.sh/tangled.sh/core v1.8.1-alpha h1:mCBXOUfzNCT1AnbMnaBrc/AgvfnxOIf5rSIescecpko= 97 - tangled.sh/tangled.sh/core v1.8.1-alpha/go.mod h1:9kSVXCu9DMszZoQ5P4Rgdpz+RHLMjbHy++53qE7EBoU= 106 + tangled.sh/tangled.sh/core v1.8.1-alpha.0.20250828210137-07b009bd6b98 h1:WovrwwBufU89zoSaStoc6+qyUTEB/LxhUCM1MqGEUwU= 107 + tangled.sh/tangled.sh/core v1.8.1-alpha.0.20250828210137-07b009bd6b98/go.mod h1:zXmPB9VMsPWpJ6Y51PWnzB1fL3w69P0IhR9rTXIfGPY=