-1
appview/db/artifact.go
-1
appview/db/artifact.go
+81
-45
appview/db/notifications.go
+81
-45
appview/db/notifications.go
···
8
"strings"
9
"time"
10
11
"tangled.org/core/appview/models"
12
"tangled.org/core/appview/pagination"
13
)
14
15
-
func (d *DB) CreateNotification(ctx context.Context, notification *models.Notification) error {
16
query := `
17
INSERT INTO notifications (recipient_did, actor_did, type, entity_type, entity_id, read, repo_id, issue_id, pull_id)
18
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
19
`
20
21
-
result, err := d.DB.ExecContext(ctx, query,
22
notification.RecipientDid,
23
notification.ActorDid,
24
string(notification.Type),
···
274
return count, nil
275
}
276
277
-
func (d *DB) MarkNotificationRead(ctx context.Context, notificationID int64, userDID string) error {
278
idFilter := FilterEq("id", notificationID)
279
recipientFilter := FilterEq("recipient_did", userDID)
280
···
286
287
args := append(idFilter.Arg(), recipientFilter.Arg()...)
288
289
-
result, err := d.DB.ExecContext(ctx, query, args...)
290
if err != nil {
291
return fmt.Errorf("failed to mark notification as read: %w", err)
292
}
···
303
return nil
304
}
305
306
-
func (d *DB) MarkAllNotificationsRead(ctx context.Context, userDID string) error {
307
recipientFilter := FilterEq("recipient_did", userDID)
308
readFilter := FilterEq("read", 0)
309
···
315
316
args := append(recipientFilter.Arg(), readFilter.Arg()...)
317
318
-
_, err := d.DB.ExecContext(ctx, query, args...)
319
if err != nil {
320
return fmt.Errorf("failed to mark all notifications as read: %w", err)
321
}
···
323
return nil
324
}
325
326
-
func (d *DB) DeleteNotification(ctx context.Context, notificationID int64, userDID string) error {
327
idFilter := FilterEq("id", notificationID)
328
recipientFilter := FilterEq("recipient_did", userDID)
329
···
334
335
args := append(idFilter.Arg(), recipientFilter.Arg()...)
336
337
-
result, err := d.DB.ExecContext(ctx, query, args...)
338
if err != nil {
339
return fmt.Errorf("failed to delete notification: %w", err)
340
}
···
351
return nil
352
}
353
354
-
func (d *DB) GetNotificationPreferences(ctx context.Context, userDID string) (*models.NotificationPreferences, error) {
355
-
userFilter := FilterEq("user_did", userDID)
356
357
query := fmt.Sprintf(`
358
-
SELECT id, user_did, repo_starred, issue_created, issue_commented, pull_created,
359
-
pull_commented, followed, pull_merged, issue_closed, email_notifications
360
-
FROM notification_preferences
361
-
WHERE %s
362
-
`, userFilter.Condition())
363
364
-
var prefs models.NotificationPreferences
365
-
err := d.DB.QueryRowContext(ctx, query, userFilter.Arg()...).Scan(
366
-
&prefs.ID,
367
-
&prefs.UserDid,
368
-
&prefs.RepoStarred,
369
-
&prefs.IssueCreated,
370
-
&prefs.IssueCommented,
371
-
&prefs.PullCreated,
372
-
&prefs.PullCommented,
373
-
&prefs.Followed,
374
-
&prefs.PullMerged,
375
-
&prefs.IssueClosed,
376
-
&prefs.EmailNotifications,
377
-
)
378
-
379
if err != nil {
380
-
if err == sql.ErrNoRows {
381
-
return &models.NotificationPreferences{
382
-
UserDid: userDID,
383
-
RepoStarred: true,
384
-
IssueCreated: true,
385
-
IssueCommented: true,
386
-
PullCreated: true,
387
-
PullCommented: true,
388
-
Followed: true,
389
-
PullMerged: true,
390
-
IssueClosed: true,
391
-
EmailNotifications: false,
392
-
}, nil
393
}
394
-
return nil, fmt.Errorf("failed to get notification preferences: %w", err)
395
}
396
397
-
return &prefs, nil
398
}
399
400
func (d *DB) UpdateNotificationPreferences(ctx context.Context, prefs *models.NotificationPreferences) error {
···
8
"strings"
9
"time"
10
11
+
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
)
15
16
+
func CreateNotification(e Execer, notification *models.Notification) error {
17
query := `
18
INSERT INTO notifications (recipient_did, actor_did, type, entity_type, entity_id, read, repo_id, issue_id, pull_id)
19
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
20
`
21
22
+
result, err := e.Exec(query,
23
notification.RecipientDid,
24
notification.ActorDid,
25
string(notification.Type),
···
275
return count, nil
276
}
277
278
+
func MarkNotificationRead(e Execer, notificationID int64, userDID string) error {
279
idFilter := FilterEq("id", notificationID)
280
recipientFilter := FilterEq("recipient_did", userDID)
281
···
287
288
args := append(idFilter.Arg(), recipientFilter.Arg()...)
289
290
+
result, err := e.Exec(query, args...)
291
if err != nil {
292
return fmt.Errorf("failed to mark notification as read: %w", err)
293
}
···
304
return nil
305
}
306
307
+
func MarkAllNotificationsRead(e Execer, userDID string) error {
308
recipientFilter := FilterEq("recipient_did", userDID)
309
readFilter := FilterEq("read", 0)
310
···
316
317
args := append(recipientFilter.Arg(), readFilter.Arg()...)
318
319
+
_, err := e.Exec(query, args...)
320
if err != nil {
321
return fmt.Errorf("failed to mark all notifications as read: %w", err)
322
}
···
324
return nil
325
}
326
327
+
func DeleteNotification(e Execer, notificationID int64, userDID string) error {
328
idFilter := FilterEq("id", notificationID)
329
recipientFilter := FilterEq("recipient_did", userDID)
330
···
335
336
args := append(idFilter.Arg(), recipientFilter.Arg()...)
337
338
+
result, err := e.Exec(query, args...)
339
if err != nil {
340
return fmt.Errorf("failed to delete notification: %w", err)
341
}
···
352
return nil
353
}
354
355
+
func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) {
356
+
prefs, err := GetNotificationPreferences(e, FilterEq("user_did", userDid))
357
+
if err != nil {
358
+
return nil, err
359
+
}
360
+
361
+
p, ok := prefs[syntax.DID(userDid)]
362
+
if !ok {
363
+
return models.DefaultNotificationPreferences(syntax.DID(userDid)), nil
364
+
}
365
+
366
+
return p, nil
367
+
}
368
+
369
+
func GetNotificationPreferences(e Execer, filters ...filter) (map[syntax.DID]*models.NotificationPreferences, error) {
370
+
prefsMap := make(map[syntax.DID]*models.NotificationPreferences)
371
+
372
+
var conditions []string
373
+
var args []any
374
+
for _, filter := range filters {
375
+
conditions = append(conditions, filter.Condition())
376
+
args = append(args, filter.Arg()...)
377
+
}
378
+
379
+
whereClause := ""
380
+
if conditions != nil {
381
+
whereClause = " where " + strings.Join(conditions, " and ")
382
+
}
383
384
query := fmt.Sprintf(`
385
+
select
386
+
id,
387
+
user_did,
388
+
repo_starred,
389
+
issue_created,
390
+
issue_commented,
391
+
pull_created,
392
+
pull_commented,
393
+
followed,
394
+
pull_merged,
395
+
issue_closed,
396
+
email_notifications
397
+
from
398
+
notification_preferences
399
+
%s
400
+
`, whereClause)
401
402
+
rows, err := e.Query(query, args...)
403
if err != nil {
404
+
return nil, err
405
+
}
406
+
defer rows.Close()
407
+
408
+
for rows.Next() {
409
+
var prefs models.NotificationPreferences
410
+
if err := rows.Scan(
411
+
&prefs.ID,
412
+
&prefs.UserDid,
413
+
&prefs.RepoStarred,
414
+
&prefs.IssueCreated,
415
+
&prefs.IssueCommented,
416
+
&prefs.PullCreated,
417
+
&prefs.PullCommented,
418
+
&prefs.Followed,
419
+
&prefs.PullMerged,
420
+
&prefs.IssueClosed,
421
+
&prefs.EmailNotifications,
422
+
); err != nil {
423
+
return nil, err
424
}
425
+
426
+
prefsMap[prefs.UserDid] = &prefs
427
+
}
428
+
429
+
if err := rows.Err(); err != nil {
430
+
return nil, err
431
}
432
433
+
return prefsMap, nil
434
}
435
436
func (d *DB) UpdateNotificationPreferences(ctx context.Context, prefs *models.NotificationPreferences) error {
+17
-1
appview/models/notifications.go
+17
-1
appview/models/notifications.go
···
2
3
import (
4
"time"
5
+
6
+
"github.com/bluesky-social/indigo/atproto/syntax"
7
)
8
9
type NotificationType string
···
71
72
type NotificationPreferences struct {
73
ID int64
74
+
UserDid syntax.DID
75
RepoStarred bool
76
IssueCreated bool
77
IssueCommented bool
···
82
IssueClosed bool
83
EmailNotifications bool
84
}
85
+
func DefaultNotificationPreferences(user syntax.DID) *NotificationPreferences {
86
+
return &NotificationPreferences{
87
+
UserDid: user,
88
+
RepoStarred: true,
89
+
IssueCreated: true,
90
+
IssueCommented: true,
91
+
PullCreated: true,
92
+
PullCommented: true,
93
+
Followed: true,
94
+
PullMerged: true,
95
+
IssueClosed: true,
96
+
EmailNotifications: false,
97
+
}
98
+
}
+4
-4
appview/notifications/notifications.go
+4
-4
appview/notifications/notifications.go
···
76
return
77
}
78
79
-
err = n.db.MarkAllNotificationsRead(r.Context(), user.Did)
80
if err != nil {
81
l.Error("failed to mark notifications as read", "err", err)
82
}
···
128
return
129
}
130
131
-
err = n.db.MarkNotificationRead(r.Context(), notificationID, userDid)
132
if err != nil {
133
http.Error(w, "Failed to mark notification as read", http.StatusInternalServerError)
134
return
···
140
func (n *Notifications) markAllRead(w http.ResponseWriter, r *http.Request) {
141
userDid := n.oauth.GetDid(r)
142
143
-
err := n.db.MarkAllNotificationsRead(r.Context(), userDid)
144
if err != nil {
145
http.Error(w, "Failed to mark all notifications as read", http.StatusInternalServerError)
146
return
···
159
return
160
}
161
162
-
err = n.db.DeleteNotification(r.Context(), notificationID, userDid)
163
if err != nil {
164
http.Error(w, "Failed to delete notification", http.StatusInternalServerError)
165
return
···
76
return
77
}
78
79
+
err = db.MarkAllNotificationsRead(n.db, user.Did)
80
if err != nil {
81
l.Error("failed to mark notifications as read", "err", err)
82
}
···
128
return
129
}
130
131
+
err = db.MarkNotificationRead(n.db, notificationID, userDid)
132
if err != nil {
133
http.Error(w, "Failed to mark notification as read", http.StatusInternalServerError)
134
return
···
140
func (n *Notifications) markAllRead(w http.ResponseWriter, r *http.Request) {
141
userDid := n.oauth.GetDid(r)
142
143
+
err := db.MarkAllNotificationsRead(n.db, userDid)
144
if err != nil {
145
http.Error(w, "Failed to mark all notifications as read", http.StatusInternalServerError)
146
return
···
159
return
160
}
161
162
+
err = db.DeleteNotification(n.db, notificationID, userDid)
163
if err != nil {
164
http.Error(w, "Failed to delete notification", http.StatusInternalServerError)
165
return
+20
-20
appview/notify/db/db.go
+20
-20
appview/notify/db/db.go
···
42
}
43
44
// check if user wants these notifications
45
-
prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did)
46
if err != nil {
47
log.Printf("NewStar: failed to get notification preferences for %s: %v", repo.Did, err)
48
return
···
59
EntityId: string(star.RepoAt),
60
RepoId: &repo.Id,
61
}
62
-
err = n.db.CreateNotification(ctx, notification)
63
if err != nil {
64
log.Printf("NewStar: failed to create notification: %v", err)
65
return
···
81
return
82
}
83
84
-
prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did)
85
if err != nil {
86
log.Printf("NewIssue: failed to get notification preferences for %s: %v", repo.Did, err)
87
return
···
100
IssueId: &issue.Id,
101
}
102
103
-
err = n.db.CreateNotification(ctx, notification)
104
if err != nil {
105
log.Printf("NewIssue: failed to create notification: %v", err)
106
return
···
129
130
// notify issue author (if not the commenter)
131
if issue.Did != comment.Did {
132
-
prefs, err := n.db.GetNotificationPreferences(ctx, issue.Did)
133
if err == nil && prefs.IssueCommented {
134
recipients[issue.Did] = true
135
} else if err != nil {
···
139
140
// notify repo owner (if not the commenter and not already added)
141
if repo.Did != comment.Did && repo.Did != issue.Did {
142
-
prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did)
143
if err == nil && prefs.IssueCommented {
144
recipients[repo.Did] = true
145
} else if err != nil {
···
159
IssueId: &issue.Id,
160
}
161
162
-
err = n.db.CreateNotification(ctx, notification)
163
if err != nil {
164
log.Printf("NewIssueComment: failed to create notification for %s: %v", recipientDid, err)
165
}
···
167
}
168
169
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
170
-
prefs, err := n.db.GetNotificationPreferences(ctx, follow.SubjectDid)
171
if err != nil {
172
log.Printf("NewFollow: failed to get notification preferences for %s: %v", follow.SubjectDid, err)
173
return
···
184
EntityId: follow.UserDid,
185
}
186
187
-
err = n.db.CreateNotification(ctx, notification)
188
if err != nil {
189
log.Printf("NewFollow: failed to create notification: %v", err)
190
return
···
206
return
207
}
208
209
-
prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did)
210
if err != nil {
211
log.Printf("NewPull: failed to get notification preferences for %s: %v", repo.Did, err)
212
return
···
225
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
226
}
227
228
-
err = n.db.CreateNotification(ctx, notification)
229
if err != nil {
230
log.Printf("NewPull: failed to create notification: %v", err)
231
return
···
256
257
// notify pull request author (if not the commenter)
258
if pull.OwnerDid != comment.OwnerDid {
259
-
prefs, err := n.db.GetNotificationPreferences(ctx, pull.OwnerDid)
260
if err == nil && prefs.PullCommented {
261
recipients[pull.OwnerDid] = true
262
} else if err != nil {
···
266
267
// notify repo owner (if not the commenter and not already added)
268
if repo.Did != comment.OwnerDid && repo.Did != pull.OwnerDid {
269
-
prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did)
270
if err == nil && prefs.PullCommented {
271
recipients[repo.Did] = true
272
} else if err != nil {
···
285
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
286
}
287
288
-
err = n.db.CreateNotification(ctx, notification)
289
if err != nil {
290
log.Printf("NewPullComment: failed to create notification for %s: %v", recipientDid, err)
291
}
···
322
}
323
324
// Check if user wants these notifications
325
-
prefs, err := n.db.GetNotificationPreferences(ctx, repo.Did)
326
if err != nil {
327
log.Printf("NewIssueClosed: failed to get notification preferences for %s: %v", repo.Did, err)
328
return
···
341
IssueId: &issue.Id,
342
}
343
344
-
err = n.db.CreateNotification(ctx, notification)
345
if err != nil {
346
log.Printf("NewIssueClosed: failed to create notification: %v", err)
347
return
···
362
}
363
364
// Check if user wants these notifications
365
-
prefs, err := n.db.GetNotificationPreferences(ctx, pull.OwnerDid)
366
if err != nil {
367
log.Printf("NewPullMerged: failed to get notification preferences for %s: %v", pull.OwnerDid, err)
368
return
···
381
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
382
}
383
384
-
err = n.db.CreateNotification(ctx, notification)
385
if err != nil {
386
log.Printf("NewPullMerged: failed to create notification: %v", err)
387
return
···
402
}
403
404
// Check if user wants these notifications - reuse pull_merged preference for now
405
-
prefs, err := n.db.GetNotificationPreferences(ctx, pull.OwnerDid)
406
if err != nil {
407
log.Printf("NewPullClosed: failed to get notification preferences for %s: %v", pull.OwnerDid, err)
408
return
···
421
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
422
}
423
424
-
err = n.db.CreateNotification(ctx, notification)
425
if err != nil {
426
log.Printf("NewPullClosed: failed to create notification: %v", err)
427
return
···
42
}
43
44
// check if user wants these notifications
45
+
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
46
if err != nil {
47
log.Printf("NewStar: failed to get notification preferences for %s: %v", repo.Did, err)
48
return
···
59
EntityId: string(star.RepoAt),
60
RepoId: &repo.Id,
61
}
62
+
err = db.CreateNotification(n.db, notification)
63
if err != nil {
64
log.Printf("NewStar: failed to create notification: %v", err)
65
return
···
81
return
82
}
83
84
+
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
85
if err != nil {
86
log.Printf("NewIssue: failed to get notification preferences for %s: %v", repo.Did, err)
87
return
···
100
IssueId: &issue.Id,
101
}
102
103
+
err = db.CreateNotification(n.db, notification)
104
if err != nil {
105
log.Printf("NewIssue: failed to create notification: %v", err)
106
return
···
129
130
// notify issue author (if not the commenter)
131
if issue.Did != comment.Did {
132
+
prefs, err := db.GetNotificationPreference(n.db, issue.Did)
133
if err == nil && prefs.IssueCommented {
134
recipients[issue.Did] = true
135
} else if err != nil {
···
139
140
// notify repo owner (if not the commenter and not already added)
141
if repo.Did != comment.Did && repo.Did != issue.Did {
142
+
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
143
if err == nil && prefs.IssueCommented {
144
recipients[repo.Did] = true
145
} else if err != nil {
···
159
IssueId: &issue.Id,
160
}
161
162
+
err = db.CreateNotification(n.db, notification)
163
if err != nil {
164
log.Printf("NewIssueComment: failed to create notification for %s: %v", recipientDid, err)
165
}
···
167
}
168
169
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
170
+
prefs, err := db.GetNotificationPreference(n.db, follow.SubjectDid)
171
if err != nil {
172
log.Printf("NewFollow: failed to get notification preferences for %s: %v", follow.SubjectDid, err)
173
return
···
184
EntityId: follow.UserDid,
185
}
186
187
+
err = db.CreateNotification(n.db, notification)
188
if err != nil {
189
log.Printf("NewFollow: failed to create notification: %v", err)
190
return
···
206
return
207
}
208
209
+
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
210
if err != nil {
211
log.Printf("NewPull: failed to get notification preferences for %s: %v", repo.Did, err)
212
return
···
225
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
226
}
227
228
+
err = db.CreateNotification(n.db, notification)
229
if err != nil {
230
log.Printf("NewPull: failed to create notification: %v", err)
231
return
···
256
257
// notify pull request author (if not the commenter)
258
if pull.OwnerDid != comment.OwnerDid {
259
+
prefs, err := db.GetNotificationPreference(n.db, pull.OwnerDid)
260
if err == nil && prefs.PullCommented {
261
recipients[pull.OwnerDid] = true
262
} else if err != nil {
···
266
267
// notify repo owner (if not the commenter and not already added)
268
if repo.Did != comment.OwnerDid && repo.Did != pull.OwnerDid {
269
+
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
270
if err == nil && prefs.PullCommented {
271
recipients[repo.Did] = true
272
} else if err != nil {
···
285
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
286
}
287
288
+
err = db.CreateNotification(n.db, notification)
289
if err != nil {
290
log.Printf("NewPullComment: failed to create notification for %s: %v", recipientDid, err)
291
}
···
322
}
323
324
// Check if user wants these notifications
325
+
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
326
if err != nil {
327
log.Printf("NewIssueClosed: failed to get notification preferences for %s: %v", repo.Did, err)
328
return
···
341
IssueId: &issue.Id,
342
}
343
344
+
err = db.CreateNotification(n.db, notification)
345
if err != nil {
346
log.Printf("NewIssueClosed: failed to create notification: %v", err)
347
return
···
362
}
363
364
// Check if user wants these notifications
365
+
prefs, err := db.GetNotificationPreference(n.db, pull.OwnerDid)
366
if err != nil {
367
log.Printf("NewPullMerged: failed to get notification preferences for %s: %v", pull.OwnerDid, err)
368
return
···
381
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
382
}
383
384
+
err = db.CreateNotification(n.db, notification)
385
if err != nil {
386
log.Printf("NewPullMerged: failed to create notification: %v", err)
387
return
···
402
}
403
404
// Check if user wants these notifications - reuse pull_merged preference for now
405
+
prefs, err := db.GetNotificationPreference(n.db, pull.OwnerDid)
406
if err != nil {
407
log.Printf("NewPullClosed: failed to get notification preferences for %s: %v", pull.OwnerDid, err)
408
return
···
421
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
422
}
423
424
+
err = db.CreateNotification(n.db, notification)
425
if err != nil {
426
log.Printf("NewPullClosed: failed to create notification: %v", err)
427
return
+3
-2
appview/settings/settings.go
+3
-2
appview/settings/settings.go
···
22
"tangled.org/core/tid"
23
24
comatproto "github.com/bluesky-social/indigo/api/atproto"
25
lexutil "github.com/bluesky-social/indigo/lex/util"
26
"github.com/gliderlabs/ssh"
27
"github.com/google/uuid"
···
91
user := s.OAuth.GetUser(r)
92
did := s.OAuth.GetDid(r)
93
94
-
prefs, err := s.Db.GetNotificationPreferences(r.Context(), did)
95
if err != nil {
96
log.Printf("failed to get notification preferences: %s", err)
97
s.Pages.Notice(w, "settings-notifications-error", "Unable to load notification preferences.")
···
110
did := s.OAuth.GetDid(r)
111
112
prefs := &models.NotificationPreferences{
113
-
UserDid: did,
114
RepoStarred: r.FormValue("repo_starred") == "on",
115
IssueCreated: r.FormValue("issue_created") == "on",
116
IssueCommented: r.FormValue("issue_commented") == "on",
···
22
"tangled.org/core/tid"
23
24
comatproto "github.com/bluesky-social/indigo/api/atproto"
25
+
"github.com/bluesky-social/indigo/atproto/syntax"
26
lexutil "github.com/bluesky-social/indigo/lex/util"
27
"github.com/gliderlabs/ssh"
28
"github.com/google/uuid"
···
92
user := s.OAuth.GetUser(r)
93
did := s.OAuth.GetDid(r)
94
95
+
prefs, err := db.GetNotificationPreference(s.Db, did)
96
if err != nil {
97
log.Printf("failed to get notification preferences: %s", err)
98
s.Pages.Notice(w, "settings-notifications-error", "Unable to load notification preferences.")
···
111
did := s.OAuth.GetDid(r)
112
113
prefs := &models.NotificationPreferences{
114
+
UserDid: syntax.DID(did),
115
RepoStarred: r.FormValue("repo_starred") == "on",
116
IssueCreated: r.FormValue("issue_created") == "on",
117
IssueCommented: r.FormValue("issue_commented") == "on",