From f8454e379a0bf1f982a41ad44b009c3910e1bd08 Mon Sep 17 00:00:00 2001 From: Will Andrews Date: Wed, 24 Sep 2025 06:40:35 +0100 Subject: [PATCH] update issues and comments in DB if they get updated on tangled. add http server to get data to test --- Dockerfile | 1 + cmd/main.go | 66 +++++++++++++++++++++++++++++++++++++++++++++ consumer.go | 13 ++++++--- database.go | 44 ++++++++++++++++++++++++++++-- docker-compose.yaml | 2 ++ go.mod | 7 ++++- go.sum | 14 ++++++++-- 7 files changed, 139 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9e9d8b7..650de8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ COPY . . RUN go mod download COPY . . +#compiling for Pi at the moment RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -a -installsuffix cgo -o tangled-alert-bot ./cmd/. FROM alpine:latest diff --git a/cmd/main.go b/cmd/main.go index 732812e..70fb3d2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,10 +2,12 @@ package main import ( "context" + "encoding/json" "errors" "fmt" "log" "log/slog" + "net/http" "os" "os/signal" "path" @@ -14,6 +16,7 @@ import ( tangledalertbot "tangled.sh/willdot.net/tangled-alert-bot" "github.com/avast/retry-go/v4" + "github.com/bugsnag/bugsnag-go" "github.com/joho/godotenv" ) @@ -37,6 +40,10 @@ func run() error { 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 = "./" @@ -54,6 +61,8 @@ func run() error { go consumeLoop(ctx, database) + go startHttpServer(ctx, database) + <-signals cancel() @@ -78,6 +87,7 @@ func consumeLoop(ctx context.Context, database *tangledalertbot.Database) { return nil } slog.Error("consume loop", "error", err) + bugsnag.Notify(err) return err } return nil @@ -85,3 +95,59 @@ func consumeLoop(ctx context.Context, database *tangledalertbot.Database) { 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) +} diff --git a/consumer.go b/consumer.go index 6bed5ed..d44110a 100644 --- a/consumer.go +++ b/consumer.go @@ -11,6 +11,7 @@ import ( "github.com/bluesky-social/jetstream/pkg/client" "github.com/bluesky-social/jetstream/pkg/client/schedulers/sequential" "github.com/bluesky-social/jetstream/pkg/models" + "github.com/bugsnag/bugsnag-go" "tangled.sh/tangled.sh/core/api/tangled" ) @@ -100,9 +101,9 @@ func (h *Handler) HandleEvent(ctx context.Context, event *models.Event) error { } switch event.Commit.Operation { - case models.CommitOperationCreate: + case models.CommitOperationCreate, models.CommitOperationUpdate: return h.handleCreateEvent(ctx, event) - // TODO: handle deletes too + // TODO: handle deletes too default: return nil } @@ -127,6 +128,7 @@ func (h *Handler) handleIssueEvent(ctx context.Context, event *models.Event) { err := json.Unmarshal(event.Commit.Record, &issue) if err != nil { + bugsnag.Notify(err) slog.Error("error unmarshalling event record to issue", "error", err) return } @@ -136,12 +138,13 @@ func (h *Handler) handleIssueEvent(ctx context.Context, event *models.Event) { createdAt, err := time.Parse(time.RFC3339, issue.CreatedAt) if err != nil { + bugsnag.Notify(err) slog.Error("parsing createdAt time from issue", "error", err, "timestamp", issue.CreatedAt) createdAt = time.Now().UTC() } body := "" if issue.Body != nil { - body = *&body + body = *issue.Body } err = h.store.CreateIssue(Issue{ AuthorDID: did, @@ -152,6 +155,7 @@ func (h *Handler) handleIssueEvent(ctx context.Context, event *models.Event) { Repo: issue.Repo, }) if err != nil { + bugsnag.Notify(err) slog.Error("create issue", "error", err, "did", did, "rkey", rkey) return } @@ -163,6 +167,7 @@ func (h *Handler) handleIssueCommentEvent(ctx context.Context, event *models.Eve err := json.Unmarshal(event.Commit.Record, &comment) if err != nil { + bugsnag.Notify(err) slog.Error("error unmarshalling event record to comment", "error", err) return } @@ -172,6 +177,7 @@ func (h *Handler) handleIssueCommentEvent(ctx context.Context, event *models.Eve createdAt, err := time.Parse(time.RFC3339, comment.CreatedAt) if err != nil { + bugsnag.Notify(err) slog.Error("parsing createdAt time from comment", "error", err, "timestamp", comment.CreatedAt) createdAt = time.Now().UTC() } @@ -184,6 +190,7 @@ func (h *Handler) handleIssueCommentEvent(ctx context.Context, event *models.Eve //ReplyTo: comment, // TODO: there should be a ReplyTo field that can be used as well once the right type is imported }) if err != nil { + bugsnag.Notify(err) slog.Error("create comment", "error", err, "did", did, "rkey", rkey) return } diff --git a/database.go b/database.go index 349496b..c97ddc6 100644 --- a/database.go +++ b/database.go @@ -125,7 +125,7 @@ func createCommentsTable(db *sql.DB) error { // CreateIssue will insert a issue into a database func (d *Database) CreateIssue(issue Issue) error { - sql := `INSERT INTO issues (authorDid, rkey, title, body, repo, createdAt) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(authorDid, rkey) DO NOTHING;` + sql := `REPLACE INTO issues (authorDid, rkey, title, body, repo, createdAt) VALUES (?, ?, ?, ?, ?, ?);` _, err := d.db.Exec(sql, issue.AuthorDID, issue.RKey, issue.Title, issue.Body, issue.Repo, issue.CreatedAt) if err != nil { return fmt.Errorf("exec insert issue: %w", err) @@ -135,10 +135,50 @@ func (d *Database) CreateIssue(issue Issue) error { // CreateComment will insert a comment into a database func (d *Database) CreateComment(comment Comment) error { - sql := `INSERT INTO comments (authorDid, rkey, body, issue, replyTo, createdAt) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(authorDid, rkey) DO NOTHING;` + sql := `REPLACE INTO comments (authorDid, rkey, body, issue, replyTo, createdAt) VALUES (?, ?, ?, ?, ?, ?);` _, err := d.db.Exec(sql, comment.AuthorDID, comment.RKey, comment.Body, comment.Issue, comment.ReplyTo, comment.CreatedAt) if err != nil { return fmt.Errorf("exec insert comment: %w", err) } return nil } + +func (d *Database) GetIssues() ([]Issue, error) { + sql := "SELECT authorDid, rkey, title, body, repo, createdAt FROM issues;" + rows, err := d.db.Query(sql) + if err != nil { + return nil, fmt.Errorf("run query to get issues: %w", err) + } + defer rows.Close() + + var results []Issue + for rows.Next() { + var issue Issue + if err := rows.Scan(&issue.AuthorDID, &issue.RKey, &issue.Title, &issue.Body, &issue.Repo, &issue.CreatedAt); err != nil { + return nil, fmt.Errorf("scan row: %w", err) + } + + results = append(results, issue) + } + return results, nil +} + +func (d *Database) GetComments() ([]Comment, error) { + sql := "SELECT authorDid, rkey, body, issue, replyTo, createdAt FROM comments;" + rows, err := d.db.Query(sql) + if err != nil { + return nil, fmt.Errorf("run query to get comments: %w", err) + } + defer rows.Close() + + var results []Comment + for rows.Next() { + var comment Comment + if err := rows.Scan(&comment.AuthorDID, &comment.RKey, &comment.Body, &comment.Issue, &comment.ReplyTo, &comment.CreatedAt); err != nil { + return nil, fmt.Errorf("scan row: %w", err) + } + + results = append(results, comment) + } + return results, nil +} diff --git a/docker-compose.yaml b/docker-compose.yaml index aa3e999..795e7c4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,6 +2,8 @@ services: tangled-alert-bot: container_name: tangled-alert-bot image: willdot/tangled-alert-bot + ports: + - "3000:3000" volumes: - ./data:/app/data environment: diff --git a/go.mod b/go.mod index 65a530b..ea5d5c3 100644 --- a/go.mod +++ b/go.mod @@ -5,20 +5,24 @@ go 1.25.0 require ( github.com/avast/retry-go/v4 v4.6.1 github.com/bluesky-social/jetstream v0.0.0-20250815235753-306e46369336 + github.com/bugsnag/bugsnag-go v2.6.2+incompatible github.com/glebarez/go-sqlite v1.22.0 github.com/joho/godotenv v1.5.1 - tangled.sh/tangled.sh/core v1.8.1-alpha + tangled.sh/tangled.sh/core v1.8.1-alpha.0.20250828210137-07b009bd6b98 ) require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/bitly/go-simplejson v0.5.1 // indirect github.com/bluesky-social/indigo v0.0.0-20250808182429-6f0837c2d12b // indirect + github.com/bugsnag/panicwrap v1.3.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/ipfs/go-cid v0.5.0 // indirect + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -30,6 +34,7 @@ require ( github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.64.0 // indirect diff --git a/go.sum b/go.sum index 189dd37..b931ebe 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,16 @@ github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIc github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/bluesky-social/indigo v0.0.0-20250808182429-6f0837c2d12b h1:bJTlFwMhB9sluuqZxVXtv2yFcaWOC/PZokz9mcwb4u4= github.com/bluesky-social/indigo v0.0.0-20250808182429-6f0837c2d12b/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q= github.com/bluesky-social/jetstream v0.0.0-20250815235753-306e46369336 h1:NM3wfeFUrdjCE/xHLXQorwQvEKlI9uqnWl7L0Y9KA8U= github.com/bluesky-social/jetstream v0.0.0-20250815235753-306e46369336/go.mod h1:3ihWQCbXeayg41G8lQ5DfB/3NnEhl0XX24eZ3mLpf7Q= +github.com/bugsnag/bugsnag-go v2.6.2+incompatible h1:6R/uadVvhrciRbPXFmCY7sZ7ElbGKsxxOvG78HcGwj8= +github.com/bugsnag/bugsnag-go v2.6.2+incompatible/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.3.4 h1:A6sXFtDGsgU/4BLf5JT0o5uYg3EeKgGx3Sfs+/uk3pU= +github.com/bugsnag/panicwrap v1.3.4/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -28,6 +34,8 @@ github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -50,6 +58,8 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= @@ -93,5 +103,5 @@ modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= -tangled.sh/tangled.sh/core v1.8.1-alpha h1:mCBXOUfzNCT1AnbMnaBrc/AgvfnxOIf5rSIescecpko= -tangled.sh/tangled.sh/core v1.8.1-alpha/go.mod h1:9kSVXCu9DMszZoQ5P4Rgdpz+RHLMjbHy++53qE7EBoU= +tangled.sh/tangled.sh/core v1.8.1-alpha.0.20250828210137-07b009bd6b98 h1:WovrwwBufU89zoSaStoc6+qyUTEB/LxhUCM1MqGEUwU= +tangled.sh/tangled.sh/core v1.8.1-alpha.0.20250828210137-07b009bd6b98/go.mod h1:zXmPB9VMsPWpJ6Y51PWnzB1fL3w69P0IhR9rTXIfGPY= -- 2.51.0