Monorepo for Tangled tangled.org

appview/notify/db: implement db notifier

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

anirudh.fi b10fe6c6 bdd21293

verified
Changed files
+474
appview
notify
db
state
+469
appview/notify/db/db.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "log" 6 + 7 + "tangled.sh/tangled.sh/core/appview/db" 8 + "tangled.sh/tangled.sh/core/appview/notify" 9 + "tangled.sh/tangled.sh/core/idresolver" 10 + ) 11 + 12 + type databaseNotifier struct { 13 + db *db.DB 14 + res *idresolver.Resolver 15 + } 16 + 17 + func NewDatabaseNotifier(database *db.DB, resolver *idresolver.Resolver) notify.Notifier { 18 + return &databaseNotifier{ 19 + db: database, 20 + res: resolver, 21 + } 22 + } 23 + 24 + var _ notify.Notifier = &databaseNotifier{} 25 + 26 + func (n *databaseNotifier) NewRepo(ctx context.Context, repo *db.Repo) { 27 + // no-op for now 28 + } 29 + 30 + func (n *databaseNotifier) NewStar(ctx context.Context, star *db.Star) { 31 + var err error 32 + repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(star.RepoAt))) 33 + if err != nil { 34 + log.Printf("NewStar: failed to get repos: %v", err) 35 + return 36 + } 37 + if len(repos) == 0 { 38 + log.Printf("NewStar: no repo found for %s", star.RepoAt) 39 + return 40 + } 41 + repo := repos[0] 42 + 43 + // don't notify yourself 44 + if repo.Did == star.StarredByDid { 45 + return 46 + } 47 + 48 + // check if user wants these notifications 49 + prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did) 50 + if err != nil { 51 + log.Printf("NewStar: failed to get notification preferences for %s: %v", repo.Did, err) 52 + return 53 + } 54 + if !prefs.RepoStarred { 55 + return 56 + } 57 + 58 + notification := &models.Notification{ 59 + RecipientDid: repo.Did, 60 + ActorDid: star.StarredByDid, 61 + Type: models.NotificationTypeRepoStarred, 62 + EntityType: "repo", 63 + EntityId: string(star.RepoAt), 64 + RepoId: &repo.ID, 65 + } 66 + 67 + err = n.db.CreateNotification(ctx, notification) 68 + if err != nil { 69 + log.Printf("NewStar: failed to create notification: %v", err) 70 + return 71 + } 72 + } 73 + 74 + func (n *databaseNotifier) DeleteStar(ctx context.Context, star *db.Star) { 75 + // no-op 76 + } 77 + 78 + func (n *databaseNotifier) NewIssue(ctx context.Context, issue *db.Issue) { 79 + repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(issue.RepoAt))) 80 + if err != nil { 81 + log.Printf("NewIssue: failed to get repos: %v", err) 82 + return 83 + } 84 + if len(repos) == 0 { 85 + log.Printf("NewIssue: no repo found for %s", issue.RepoAt) 86 + return 87 + } 88 + repo := repos[0] 89 + 90 + if repo.Did == issue.Did { 91 + return 92 + } 93 + 94 + prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did) 95 + if err != nil { 96 + log.Printf("NewIssue: failed to get notification preferences for %s: %v", repo.Did, err) 97 + return 98 + } 99 + if !prefs.IssueCreated { 100 + return 101 + } 102 + 103 + notification := &models.Notification{ 104 + RecipientDid: repo.Did, 105 + ActorDid: issue.Did, 106 + Type: models.NotificationTypeIssueCreated, 107 + EntityType: "issue", 108 + EntityId: string(issue.AtUri()), 109 + RepoId: &repo.ID, 110 + IssueId: &issue.Id, 111 + } 112 + 113 + err = n.db.CreateNotification(ctx, notification) 114 + if err != nil { 115 + log.Printf("NewIssue: failed to create notification: %v", err) 116 + return 117 + } 118 + } 119 + 120 + func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *db.IssueComment) { 121 + issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt)) 122 + if err != nil { 123 + log.Printf("NewIssueComment: failed to get issues: %v", err) 124 + return 125 + } 126 + if len(issues) == 0 { 127 + log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt) 128 + return 129 + } 130 + issue := issues[0] 131 + 132 + repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(issue.RepoAt))) 133 + if err != nil { 134 + log.Printf("NewIssueComment: failed to get repos: %v", err) 135 + return 136 + } 137 + if len(repos) == 0 { 138 + log.Printf("NewIssueComment: no repo found for %s", issue.RepoAt) 139 + return 140 + } 141 + repo := repos[0] 142 + 143 + recipients := make(map[string]bool) 144 + 145 + // notify issue author (if not the commenter) 146 + if issue.Did != comment.Did { 147 + prefs, err := n.db.GetNotificationPreferences(ctx, issue.Did) 148 + if err == nil && prefs.IssueCommented { 149 + recipients[issue.Did] = true 150 + } else if err != nil { 151 + log.Printf("NewIssueComment: failed to get preferences for issue author %s: %v", issue.Did, err) 152 + } 153 + } 154 + 155 + // notify repo owner (if not the commenter and not already added) 156 + if repo.Did != comment.Did && repo.Did != issue.Did { 157 + prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did) 158 + if err == nil && prefs.IssueCommented { 159 + recipients[repo.Did] = true 160 + } else if err != nil { 161 + log.Printf("NewIssueComment: failed to get preferences for repo owner %s: %v", repo.Did, err) 162 + } 163 + } 164 + 165 + // create notifications for all recipients 166 + for recipientDid := range recipients { 167 + notification := &models.Notification{ 168 + RecipientDid: recipientDid, 169 + ActorDid: comment.Did, 170 + Type: models.NotificationTypeIssueCommented, 171 + EntityType: "issue", 172 + EntityId: string(issue.AtUri()), 173 + RepoId: &repo.ID, 174 + IssueId: &issue.Id, 175 + } 176 + 177 + err = n.db.CreateNotification(ctx, notification) 178 + if err != nil { 179 + log.Printf("NewIssueComment: failed to create notification for %s: %v", recipientDid, err) 180 + } 181 + } 182 + } 183 + 184 + func (n *databaseNotifier) NewFollow(ctx context.Context, follow *db.Follow) { 185 + prefs, err := n.db.GetNotificationPreferences(ctx, follow.SubjectDid) 186 + if err != nil { 187 + log.Printf("NewFollow: failed to get notification preferences for %s: %v", follow.SubjectDid, err) 188 + return 189 + } 190 + if !prefs.Followed { 191 + return 192 + } 193 + 194 + notification := &models.Notification{ 195 + RecipientDid: follow.SubjectDid, 196 + ActorDid: follow.UserDid, 197 + Type: models.NotificationTypeFollowed, 198 + EntityType: "follow", 199 + EntityId: follow.UserDid, 200 + } 201 + 202 + err = n.db.CreateNotification(ctx, notification) 203 + if err != nil { 204 + log.Printf("NewFollow: failed to create notification: %v", err) 205 + return 206 + } 207 + } 208 + 209 + func (n *databaseNotifier) DeleteFollow(ctx context.Context, follow *db.Follow) { 210 + // no-op 211 + } 212 + 213 + func (n *databaseNotifier) NewPull(ctx context.Context, pull *db.Pull) { 214 + repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(pull.RepoAt))) 215 + if err != nil { 216 + log.Printf("NewPull: failed to get repos: %v", err) 217 + return 218 + } 219 + if len(repos) == 0 { 220 + log.Printf("NewPull: no repo found for %s", pull.RepoAt) 221 + return 222 + } 223 + repo := repos[0] 224 + 225 + if repo.Did == pull.OwnerDid { 226 + return 227 + } 228 + 229 + prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did) 230 + if err != nil { 231 + log.Printf("NewPull: failed to get notification preferences for %s: %v", repo.Did, err) 232 + return 233 + } 234 + if !prefs.PullCreated { 235 + return 236 + } 237 + 238 + notification := &models.Notification{ 239 + RecipientDid: repo.Did, 240 + ActorDid: pull.OwnerDid, 241 + Type: models.NotificationTypePullCreated, 242 + EntityType: "pull", 243 + EntityId: string(pull.RepoAt), 244 + RepoId: &repo.ID, 245 + PullId: func() *int64 { id := int64(pull.ID); return &id }(), 246 + } 247 + 248 + err = n.db.CreateNotification(ctx, notification) 249 + if err != nil { 250 + log.Printf("NewPull: failed to create notification: %v", err) 251 + return 252 + } 253 + } 254 + 255 + func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *db.PullComment) { 256 + pulls, err := db.GetPulls(n.db, 257 + db.FilterEq("repo_at", comment.RepoAt), 258 + db.FilterEq("pull_id", comment.PullId)) 259 + if err != nil { 260 + log.Printf("NewPullComment: failed to get pulls: %v", err) 261 + return 262 + } 263 + if len(pulls) == 0 { 264 + log.Printf("NewPullComment: no pull found for %s PR %d", comment.RepoAt, comment.PullId) 265 + return 266 + } 267 + pull := pulls[0] 268 + 269 + repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", comment.RepoAt)) 270 + if err != nil { 271 + log.Printf("NewPullComment: failed to get repos: %v", err) 272 + return 273 + } 274 + if len(repos) == 0 { 275 + log.Printf("NewPullComment: no repo found for %s", comment.RepoAt) 276 + return 277 + } 278 + repo := repos[0] 279 + 280 + recipients := make(map[string]bool) 281 + 282 + // notify pull request author (if not the commenter) 283 + if pull.OwnerDid != comment.OwnerDid { 284 + prefs, err := n.db.GetNotificationPreferences(ctx, pull.OwnerDid) 285 + if err == nil && prefs.PullCommented { 286 + recipients[pull.OwnerDid] = true 287 + } else if err != nil { 288 + log.Printf("NewPullComment: failed to get preferences for pull author %s: %v", pull.OwnerDid, err) 289 + } 290 + } 291 + 292 + // notify repo owner (if not the commenter and not already added) 293 + if repo.Did != comment.OwnerDid && repo.Did != pull.OwnerDid { 294 + prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did) 295 + if err == nil && prefs.PullCommented { 296 + recipients[repo.Did] = true 297 + } else if err != nil { 298 + log.Printf("NewPullComment: failed to get preferences for repo owner %s: %v", repo.Did, err) 299 + } 300 + } 301 + 302 + for recipientDid := range recipients { 303 + notification := &models.Notification{ 304 + RecipientDid: recipientDid, 305 + ActorDid: comment.OwnerDid, 306 + Type: models.NotificationTypePullCommented, 307 + EntityType: "pull", 308 + EntityId: comment.RepoAt, 309 + RepoId: &repo.ID, 310 + PullId: func() *int64 { id := int64(pull.ID); return &id }(), 311 + } 312 + 313 + err = n.db.CreateNotification(ctx, notification) 314 + if err != nil { 315 + log.Printf("NewPullComment: failed to create notification for %s: %v", recipientDid, err) 316 + } 317 + } 318 + } 319 + 320 + func (n *databaseNotifier) UpdateProfile(ctx context.Context, profile *db.Profile) { 321 + // no-op 322 + } 323 + 324 + func (n *databaseNotifier) DeleteString(ctx context.Context, did, rkey string) { 325 + // no-op 326 + } 327 + 328 + func (n *databaseNotifier) EditString(ctx context.Context, string *db.String) { 329 + // no-op 330 + } 331 + 332 + func (n *databaseNotifier) NewString(ctx context.Context, string *db.String) { 333 + // no-op 334 + } 335 + 336 + func (n *databaseNotifier) NewIssueClosed(ctx context.Context, issue *db.Issue) { 337 + // Get repo details 338 + repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(issue.RepoAt))) 339 + if err != nil { 340 + log.Printf("NewIssueClosed: failed to get repos: %v", err) 341 + return 342 + } 343 + if len(repos) == 0 { 344 + log.Printf("NewIssueClosed: no repo found for %s", issue.RepoAt) 345 + return 346 + } 347 + repo := repos[0] 348 + 349 + // Don't notify yourself 350 + if repo.Did == issue.Did { 351 + return 352 + } 353 + 354 + // Check if user wants these notifications 355 + prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did) 356 + if err != nil { 357 + log.Printf("NewIssueClosed: failed to get notification preferences for %s: %v", repo.Did, err) 358 + return 359 + } 360 + if !prefs.IssueClosed { 361 + return 362 + } 363 + 364 + notification := &models.Notification{ 365 + RecipientDid: repo.Did, 366 + ActorDid: issue.Did, 367 + Type: models.NotificationTypeIssueClosed, 368 + EntityType: "issue", 369 + EntityId: string(issue.AtUri()), 370 + RepoId: &repo.ID, 371 + IssueId: &issue.Id, 372 + } 373 + 374 + err = n.db.CreateNotification(ctx, notification) 375 + if err != nil { 376 + log.Printf("NewIssueClosed: failed to create notification: %v", err) 377 + return 378 + } 379 + } 380 + 381 + func (n *databaseNotifier) NewPullMerged(ctx context.Context, pull *db.Pull) { 382 + // Get repo details 383 + repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(pull.RepoAt))) 384 + if err != nil { 385 + log.Printf("NewPullMerged: failed to get repos: %v", err) 386 + return 387 + } 388 + if len(repos) == 0 { 389 + log.Printf("NewPullMerged: no repo found for %s", pull.RepoAt) 390 + return 391 + } 392 + repo := repos[0] 393 + 394 + // Don't notify yourself 395 + if repo.Did == pull.OwnerDid { 396 + return 397 + } 398 + 399 + // Check if user wants these notifications 400 + prefs, err := n.db.GetNotificationPreferences(ctx, pull.OwnerDid) 401 + if err != nil { 402 + log.Printf("NewPullMerged: failed to get notification preferences for %s: %v", pull.OwnerDid, err) 403 + return 404 + } 405 + if !prefs.PullMerged { 406 + return 407 + } 408 + 409 + notification := &models.Notification{ 410 + RecipientDid: pull.OwnerDid, 411 + ActorDid: repo.Did, 412 + Type: models.NotificationTypePullMerged, 413 + EntityType: "pull", 414 + EntityId: string(pull.RepoAt), 415 + RepoId: &repo.ID, 416 + PullId: func() *int64 { id := int64(pull.ID); return &id }(), 417 + } 418 + 419 + err = n.db.CreateNotification(ctx, notification) 420 + if err != nil { 421 + log.Printf("NewPullMerged: failed to create notification: %v", err) 422 + return 423 + } 424 + } 425 + 426 + func (n *databaseNotifier) NewPullClosed(ctx context.Context, pull *db.Pull) { 427 + // Get repo details 428 + repos, err := db.GetRepos(n.db, 1, db.FilterEq("at_uri", string(pull.RepoAt))) 429 + if err != nil { 430 + log.Printf("NewPullClosed: failed to get repos: %v", err) 431 + return 432 + } 433 + if len(repos) == 0 { 434 + log.Printf("NewPullClosed: no repo found for %s", pull.RepoAt) 435 + return 436 + } 437 + repo := repos[0] 438 + 439 + // Don't notify yourself 440 + if repo.Did == pull.OwnerDid { 441 + return 442 + } 443 + 444 + // Check if user wants these notifications - reuse pull_merged preference for now 445 + prefs, err := n.db.GetNotificationPreferences(ctx, pull.OwnerDid) 446 + if err != nil { 447 + log.Printf("NewPullClosed: failed to get notification preferences for %s: %v", pull.OwnerDid, err) 448 + return 449 + } 450 + if !prefs.PullMerged { 451 + return 452 + } 453 + 454 + notification := &models.Notification{ 455 + RecipientDid: pull.OwnerDid, 456 + ActorDid: repo.Did, 457 + Type: models.NotificationTypePullClosed, 458 + EntityType: "pull", 459 + EntityId: string(pull.RepoAt), 460 + RepoId: &repo.ID, 461 + PullId: func() *int64 { id := int64(pull.ID); return &id }(), 462 + } 463 + 464 + err = n.db.CreateNotification(ctx, notification) 465 + if err != nil { 466 + log.Printf("NewPullClosed: failed to create notification: %v", err) 467 + return 468 + } 469 + }
+5
appview/state/state.go
··· 143 143 spindlestream.Start(ctx) 144 144 145 145 var notifiers []notify.Notifier 146 + 147 + // Always add the database notifier 148 + notifiers = append(notifiers, dbnotify.NewDatabaseNotifier(d, res)) 149 + 150 + // Add other notifiers in production only 146 151 if !config.Core.Dev { 147 152 notifiers = append(notifiers, phnotify.NewPosthogNotifier(posthog)) 148 153 }