From e2615e9f8ebdd9865d237bb7a4c18b414ed52fa3 Mon Sep 17 00:00:00 2001 From: Will Andrews Date: Wed, 24 Sep 2025 21:20:35 +0100 Subject: [PATCH] listen to delete events and delete issues or comments --- cmd/main.go | 2 -- consumer.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++------ database.go | 36 +++++++++++++++++++++---- 3 files changed, 99 insertions(+), 15 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 70fb3d2..705b8ff 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -86,8 +86,6 @@ func consumeLoop(ctx context.Context, database *tangledalertbot.Database) { if errors.Is(err, context.Canceled) { return nil } - slog.Error("consume loop", "error", err) - bugsnag.Notify(err) return err } return nil diff --git a/consumer.go b/consumer.go index d44110a..8c6401f 100644 --- a/consumer.go +++ b/consumer.go @@ -29,13 +29,15 @@ type Comment struct { RKey string `json:"rkey"` Body string `json:"body"` Issue string `json:"issue" ` - ReplyTo string `json:"replyTo"` CreatedAt int64 `json:"createdAt"` } type Store interface { CreateIssue(issue Issue) error CreateComment(comment Comment) error + DeleteIssue(did, rkey string) error + DeleteComment(did, rkey string) error + DeleteCommentsForIssue(issueURI string) error } // JetstreamConsumer is responsible for consuming from a jetstream instance @@ -102,19 +104,20 @@ func (h *Handler) HandleEvent(ctx context.Context, event *models.Event) error { switch event.Commit.Operation { case models.CommitOperationCreate, models.CommitOperationUpdate: - return h.handleCreateEvent(ctx, event) - // TODO: handle deletes too + return h.handleCreateUpdateEvent(ctx, event) + case models.CommitOperationDelete: + return h.handleDeleteEvent(ctx, event) default: return nil } } -func (h *Handler) handleCreateEvent(ctx context.Context, event *models.Event) error { +func (h *Handler) handleCreateUpdateEvent(ctx context.Context, event *models.Event) error { switch event.Commit.Collection { case tangled.RepoIssueNSID: - h.handleIssueEvent(ctx, event) + h.handleCreateUpdateIssueEvent(ctx, event) case tangled.RepoIssueCommentNSID: - h.handleIssueCommentEvent(ctx, event) + h.handleCreateUpdateIssueCommentEvent(ctx, event) default: slog.Info("create event was not for expected collection", "RKey", "did", event.Did, event.Commit.RKey, "collection", event.Commit.Collection) return nil @@ -123,7 +126,21 @@ func (h *Handler) handleCreateEvent(ctx context.Context, event *models.Event) er return nil } -func (h *Handler) handleIssueEvent(ctx context.Context, event *models.Event) { +func (h *Handler) handleDeleteEvent(ctx context.Context, event *models.Event) error { + switch event.Commit.Collection { + case tangled.RepoIssueNSID: + h.handleDeleteIssueEvent(ctx, event) + case tangled.RepoIssueCommentNSID: + h.handleDeleteIssueCommentEvent(ctx, event) + default: + slog.Info("create event was not for expected collection", "RKey", "did", event.Did, event.Commit.RKey, "collection", event.Commit.Collection) + return nil + } + + return nil +} + +func (h *Handler) handleCreateUpdateIssueEvent(ctx context.Context, event *models.Event) { var issue tangled.RepoIssue err := json.Unmarshal(event.Commit.Record, &issue) @@ -162,7 +179,7 @@ func (h *Handler) handleIssueEvent(ctx context.Context, event *models.Event) { slog.Info("created issue ", "value", issue, "did", did, "rkey", rkey) } -func (h *Handler) handleIssueCommentEvent(ctx context.Context, event *models.Event) { +func (h *Handler) handleCreateUpdateIssueCommentEvent(ctx context.Context, event *models.Event) { var comment tangled.RepoIssueComment err := json.Unmarshal(event.Commit.Record, &comment) @@ -181,6 +198,10 @@ func (h *Handler) handleIssueCommentEvent(ctx context.Context, event *models.Eve slog.Error("parsing createdAt time from comment", "error", err, "timestamp", comment.CreatedAt) createdAt = time.Now().UTC() } + + // TODO: if there is a reply to present, don't store the comment because replies can't be replied to so + // the reply comment doesn't need to be stored + err = h.store.CreateComment(Comment{ AuthorDID: did, RKey: rkey, @@ -199,3 +220,42 @@ func (h *Handler) handleIssueCommentEvent(ctx context.Context, event *models.Eve slog.Info("created comment ", "value", comment, "did", did, "rkey", rkey) } + +func (h *Handler) handleDeleteIssueEvent(ctx context.Context, event *models.Event) { + did := event.Did + rkey := event.Commit.RKey + + err := h.store.DeleteIssue(did, rkey) + if err != nil { + bugsnag.Notify(err) + slog.Error("delete issue", "error", err, "did", did, "rkey", rkey) + return + } + + // now attempt to delete any comments on that issue since they can't be replied to now. + // Note: if unsuccessful it doesn't matter because a deleted issue and its comments are + // not visible on the UI and so no one will be able to reply to them so this is just a + // cleanup operation + issueURI := fmt.Sprintf("at://%s/%s/%s", did, tangled.RepoIssueNSID, rkey) + err = h.store.DeleteCommentsForIssue(issueURI) + if err != nil { + bugsnag.Notify(err) + slog.Error("delete comments for issue", "error", err, "issue URI", issueURI) + } + + slog.Info("deleted issue ", "did", did, "rkey", rkey) +} + +func (h *Handler) handleDeleteIssueCommentEvent(ctx context.Context, event *models.Event) { + did := event.Did + rkey := event.Commit.RKey + + err := h.store.DeleteComment(did, rkey) + if err != nil { + bugsnag.Notify(err) + slog.Error("delete comment", "error", err, "did", did, "rkey", rkey) + return + } + + slog.Info("deleted comment ", "did", did, "rkey", rkey) +} diff --git a/database.go b/database.go index c97ddc6..1e21d83 100644 --- a/database.go +++ b/database.go @@ -104,7 +104,6 @@ func createCommentsTable(db *sql.DB) error { "rkey" TEXT, "body" TEXT, "issue" TEXT, - "replyTo" TEXT, "createdAt" integer NOT NULL, UNIQUE(authorDid,rkey) );` @@ -135,8 +134,8 @@ func (d *Database) CreateIssue(issue Issue) error { // CreateComment will insert a comment into a database func (d *Database) CreateComment(comment Comment) error { - 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) + sql := `REPLACE INTO comments (authorDid, rkey, body, issue, createdAt) VALUES (?, ?, ?, ?, ?);` + _, err := d.db.Exec(sql, comment.AuthorDID, comment.RKey, comment.Body, comment.Issue, comment.CreatedAt) if err != nil { return fmt.Errorf("exec insert comment: %w", err) } @@ -164,7 +163,7 @@ func (d *Database) GetIssues() ([]Issue, error) { } func (d *Database) GetComments() ([]Comment, error) { - sql := "SELECT authorDid, rkey, body, issue, replyTo, createdAt FROM comments;" + sql := "SELECT authorDid, rkey, body, issue, createdAt FROM comments;" rows, err := d.db.Query(sql) if err != nil { return nil, fmt.Errorf("run query to get comments: %w", err) @@ -174,7 +173,7 @@ func (d *Database) GetComments() ([]Comment, error) { 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 { + if err := rows.Scan(&comment.AuthorDID, &comment.RKey, &comment.Body, &comment.Issue, &comment.CreatedAt); err != nil { return nil, fmt.Errorf("scan row: %w", err) } @@ -182,3 +181,30 @@ func (d *Database) GetComments() ([]Comment, error) { } return results, nil } + +func (d *Database) DeleteIssue(did, rkey string) error { + sql := "DELETE FROM issues WHERE authorDid = ? AND rkey = ?;" + _, err := d.db.Exec(sql, did, rkey) + if err != nil { + return fmt.Errorf("exec delete issue: %w", err) + } + return nil +} + +func (d *Database) DeleteComment(did, rkey string) error { + sql := "DELETE FROM comments WHERE authorDid = ? AND rkey = ?;" + _, err := d.db.Exec(sql, did, rkey) + if err != nil { + return fmt.Errorf("exec delete issue: %w", err) + } + return nil +} + +func (d *Database) DeleteCommentsForIssue(issueURI string) error { + sql := "DELETE FROM comments WHERE issue = ?;" + _, err := d.db.Exec(sql, issueURI) + if err != nil { + return fmt.Errorf("exec delete comments for issue") + } + return nil +} -- 2.51.0