Delete issues and comments when deleted on Tangled #2

merged
opened by willdot.net targeting main from handle-deletes

Listens to delete events from Jetstream and then deletes the relevant data.

Changed files
+99 -15
cmd
-2
cmd/main.go
···
if errors.Is(err, context.Canceled) {
return nil
}
-
slog.Error("consume loop", "error", err)
-
bugsnag.Notify(err)
return err
}
return nil
+68 -8
consumer.go
···
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
···
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
···
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)
···
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)
···
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,
···
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)
+
}
+31 -5
database.go
···
"rkey" TEXT,
"body" TEXT,
"issue" TEXT,
-
"replyTo" TEXT,
"createdAt" integer NOT NULL,
UNIQUE(authorDid,rkey)
);`
···
// 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)
}
···
}
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)
···
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)
}
···
}
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
+
}