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

+1
Dockerfile
···
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
+66
cmd/main.go
···
import (
"context"
+
"encoding/json"
"errors"
"fmt"
"log"
"log/slog"
+
"net/http"
"os"
"os/signal"
"path"
···
tangledalertbot "tangled.sh/willdot.net/tangled-alert-bot"
"github.com/avast/retry-go/v4"
+
"github.com/bugsnag/bugsnag-go"
"github.com/joho/godotenv"
)
···
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 = "./"
···
defer cancel()
go consumeLoop(ctx, database)
+
+
go startHttpServer(ctx, database)
<-signals
cancel()
···
return nil
}
slog.Error("consume loop", "error", err)
+
bugsnag.Notify(err)
return err
}
return nil
···
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)
+
}
+10 -3
consumer.go
···
"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"
)
···
}
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
}
···
err := json.Unmarshal(event.Commit.Record, &issue)
if err != nil {
+
bugsnag.Notify(err)
slog.Error("error unmarshalling event record to issue", "error", err)
return
}
···
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,
···
Repo: issue.Repo,
})
if err != nil {
+
bugsnag.Notify(err)
slog.Error("create issue", "error", err, "did", did, "rkey", rkey)
return
}
···
err := json.Unmarshal(event.Commit.Record, &comment)
if err != nil {
+
bugsnag.Notify(err)
slog.Error("error unmarshalling event record to comment", "error", err)
return
}
···
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()
}
···
//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
}
+42 -2
database.go
···
// 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)
···
// 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
+
}
+2
docker-compose.yaml
···
tangled-alert-bot:
container_name: tangled-alert-bot
image: willdot/tangled-alert-bot
+
ports:
+
- "3000:3000"
volumes:
- ./data:/app/data
environment:
+6 -1
go.mod
···
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
···
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
+12 -2
go.sum
···
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=
···
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=
···
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=
···
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=