this repo has no description

listen to delete events and delete issues or comments

Changed files
+99 -15
cmd
-2
cmd/main.go
··· 86 86 if errors.Is(err, context.Canceled) { 87 87 return nil 88 88 } 89 - slog.Error("consume loop", "error", err) 90 - bugsnag.Notify(err) 91 89 return err 92 90 } 93 91 return nil
+68 -8
consumer.go
··· 29 29 RKey string `json:"rkey"` 30 30 Body string `json:"body"` 31 31 Issue string `json:"issue" ` 32 - ReplyTo string `json:"replyTo"` 33 32 CreatedAt int64 `json:"createdAt"` 34 33 } 35 34 36 35 type Store interface { 37 36 CreateIssue(issue Issue) error 38 37 CreateComment(comment Comment) error 38 + DeleteIssue(did, rkey string) error 39 + DeleteComment(did, rkey string) error 40 + DeleteCommentsForIssue(issueURI string) error 39 41 } 40 42 41 43 // JetstreamConsumer is responsible for consuming from a jetstream instance ··· 102 104 103 105 switch event.Commit.Operation { 104 106 case models.CommitOperationCreate, models.CommitOperationUpdate: 105 - return h.handleCreateEvent(ctx, event) 106 - // TODO: handle deletes too 107 + return h.handleCreateUpdateEvent(ctx, event) 108 + case models.CommitOperationDelete: 109 + return h.handleDeleteEvent(ctx, event) 107 110 default: 108 111 return nil 109 112 } 110 113 } 111 114 112 - func (h *Handler) handleCreateEvent(ctx context.Context, event *models.Event) error { 115 + func (h *Handler) handleCreateUpdateEvent(ctx context.Context, event *models.Event) error { 113 116 switch event.Commit.Collection { 114 117 case tangled.RepoIssueNSID: 115 - h.handleIssueEvent(ctx, event) 118 + h.handleCreateUpdateIssueEvent(ctx, event) 116 119 case tangled.RepoIssueCommentNSID: 117 - h.handleIssueCommentEvent(ctx, event) 120 + h.handleCreateUpdateIssueCommentEvent(ctx, event) 118 121 default: 119 122 slog.Info("create event was not for expected collection", "RKey", "did", event.Did, event.Commit.RKey, "collection", event.Commit.Collection) 120 123 return nil ··· 123 126 return nil 124 127 } 125 128 126 - func (h *Handler) handleIssueEvent(ctx context.Context, event *models.Event) { 129 + func (h *Handler) handleDeleteEvent(ctx context.Context, event *models.Event) error { 130 + switch event.Commit.Collection { 131 + case tangled.RepoIssueNSID: 132 + h.handleDeleteIssueEvent(ctx, event) 133 + case tangled.RepoIssueCommentNSID: 134 + h.handleDeleteIssueCommentEvent(ctx, event) 135 + default: 136 + slog.Info("create event was not for expected collection", "RKey", "did", event.Did, event.Commit.RKey, "collection", event.Commit.Collection) 137 + return nil 138 + } 139 + 140 + return nil 141 + } 142 + 143 + func (h *Handler) handleCreateUpdateIssueEvent(ctx context.Context, event *models.Event) { 127 144 var issue tangled.RepoIssue 128 145 129 146 err := json.Unmarshal(event.Commit.Record, &issue) ··· 162 179 slog.Info("created issue ", "value", issue, "did", did, "rkey", rkey) 163 180 } 164 181 165 - func (h *Handler) handleIssueCommentEvent(ctx context.Context, event *models.Event) { 182 + func (h *Handler) handleCreateUpdateIssueCommentEvent(ctx context.Context, event *models.Event) { 166 183 var comment tangled.RepoIssueComment 167 184 168 185 err := json.Unmarshal(event.Commit.Record, &comment) ··· 181 198 slog.Error("parsing createdAt time from comment", "error", err, "timestamp", comment.CreatedAt) 182 199 createdAt = time.Now().UTC() 183 200 } 201 + 202 + // TODO: if there is a reply to present, don't store the comment because replies can't be replied to so 203 + // the reply comment doesn't need to be stored 204 + 184 205 err = h.store.CreateComment(Comment{ 185 206 AuthorDID: did, 186 207 RKey: rkey, ··· 199 220 200 221 slog.Info("created comment ", "value", comment, "did", did, "rkey", rkey) 201 222 } 223 + 224 + func (h *Handler) handleDeleteIssueEvent(ctx context.Context, event *models.Event) { 225 + did := event.Did 226 + rkey := event.Commit.RKey 227 + 228 + err := h.store.DeleteIssue(did, rkey) 229 + if err != nil { 230 + bugsnag.Notify(err) 231 + slog.Error("delete issue", "error", err, "did", did, "rkey", rkey) 232 + return 233 + } 234 + 235 + // now attempt to delete any comments on that issue since they can't be replied to now. 236 + // Note: if unsuccessful it doesn't matter because a deleted issue and its comments are 237 + // not visible on the UI and so no one will be able to reply to them so this is just a 238 + // cleanup operation 239 + issueURI := fmt.Sprintf("at://%s/%s/%s", did, tangled.RepoIssueNSID, rkey) 240 + err = h.store.DeleteCommentsForIssue(issueURI) 241 + if err != nil { 242 + bugsnag.Notify(err) 243 + slog.Error("delete comments for issue", "error", err, "issue URI", issueURI) 244 + } 245 + 246 + slog.Info("deleted issue ", "did", did, "rkey", rkey) 247 + } 248 + 249 + func (h *Handler) handleDeleteIssueCommentEvent(ctx context.Context, event *models.Event) { 250 + did := event.Did 251 + rkey := event.Commit.RKey 252 + 253 + err := h.store.DeleteComment(did, rkey) 254 + if err != nil { 255 + bugsnag.Notify(err) 256 + slog.Error("delete comment", "error", err, "did", did, "rkey", rkey) 257 + return 258 + } 259 + 260 + slog.Info("deleted comment ", "did", did, "rkey", rkey) 261 + }
+31 -5
database.go
··· 104 104 "rkey" TEXT, 105 105 "body" TEXT, 106 106 "issue" TEXT, 107 - "replyTo" TEXT, 108 107 "createdAt" integer NOT NULL, 109 108 UNIQUE(authorDid,rkey) 110 109 );` ··· 135 134 136 135 // CreateComment will insert a comment into a database 137 136 func (d *Database) CreateComment(comment Comment) error { 138 - sql := `REPLACE INTO comments (authorDid, rkey, body, issue, replyTo, createdAt) VALUES (?, ?, ?, ?, ?, ?);` 139 - _, err := d.db.Exec(sql, comment.AuthorDID, comment.RKey, comment.Body, comment.Issue, comment.ReplyTo, comment.CreatedAt) 137 + sql := `REPLACE INTO comments (authorDid, rkey, body, issue, createdAt) VALUES (?, ?, ?, ?, ?);` 138 + _, err := d.db.Exec(sql, comment.AuthorDID, comment.RKey, comment.Body, comment.Issue, comment.CreatedAt) 140 139 if err != nil { 141 140 return fmt.Errorf("exec insert comment: %w", err) 142 141 } ··· 164 163 } 165 164 166 165 func (d *Database) GetComments() ([]Comment, error) { 167 - sql := "SELECT authorDid, rkey, body, issue, replyTo, createdAt FROM comments;" 166 + sql := "SELECT authorDid, rkey, body, issue, createdAt FROM comments;" 168 167 rows, err := d.db.Query(sql) 169 168 if err != nil { 170 169 return nil, fmt.Errorf("run query to get comments: %w", err) ··· 174 173 var results []Comment 175 174 for rows.Next() { 176 175 var comment Comment 177 - if err := rows.Scan(&comment.AuthorDID, &comment.RKey, &comment.Body, &comment.Issue, &comment.ReplyTo, &comment.CreatedAt); err != nil { 176 + if err := rows.Scan(&comment.AuthorDID, &comment.RKey, &comment.Body, &comment.Issue, &comment.CreatedAt); err != nil { 178 177 return nil, fmt.Errorf("scan row: %w", err) 179 178 } 180 179 ··· 182 181 } 183 182 return results, nil 184 183 } 184 + 185 + func (d *Database) DeleteIssue(did, rkey string) error { 186 + sql := "DELETE FROM issues WHERE authorDid = ? AND rkey = ?;" 187 + _, err := d.db.Exec(sql, did, rkey) 188 + if err != nil { 189 + return fmt.Errorf("exec delete issue: %w", err) 190 + } 191 + return nil 192 + } 193 + 194 + func (d *Database) DeleteComment(did, rkey string) error { 195 + sql := "DELETE FROM comments WHERE authorDid = ? AND rkey = ?;" 196 + _, err := d.db.Exec(sql, did, rkey) 197 + if err != nil { 198 + return fmt.Errorf("exec delete issue: %w", err) 199 + } 200 + return nil 201 + } 202 + 203 + func (d *Database) DeleteCommentsForIssue(issueURI string) error { 204 + sql := "DELETE FROM comments WHERE issue = ?;" 205 + _, err := d.db.Exec(sql, issueURI) 206 + if err != nil { 207 + return fmt.Errorf("exec delete comments for issue") 208 + } 209 + return nil 210 + }