Yōten: A social platform for tracking the essential points of your language learning
yoten.app
1package db
2
3import (
4 "database/sql"
5 "fmt"
6 "time"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 "yoten.app/internal/domain"
10)
11
12func CreateNotification(e Execer, recipientDID, actorDID, subjectURI string, notificationType domain.NotificationType) error {
13 query := `
14 insert into notifications
15 (recipient_did, actor_did, subject_uri, type)
16 values (?, ?, ?, ?)
17 `
18
19 _, err := e.Exec(query, recipientDID, actorDID, subjectURI, notificationType)
20 if err != nil {
21 return fmt.Errorf("failed to insert notification: %w", err)
22 }
23
24 return nil
25}
26
27func Notifications(e Execer, did string, limit, offset int) ([]domain.Notification, error) {
28 // for mention-type notifications the subject_uri is the comment at-uri.
29 // we left-join comments so we can resolve the study session uri needed for
30 // building the anchor link in the ui.
31 // TODO: we should simplify this modal. perhaps we can use unions to handle
32 // the different notification types?
33
34 query := `
35 select
36 n.id,
37 n.recipient_did,
38 n.actor_did,
39 n.subject_uri,
40 n.state,
41 n.type,
42 n.created_at,
43 c.study_session_uri
44 from
45 notifications n
46 left join comments c
47 on n.type = 'mention'
48 and ('at://' || c.did || '/app.yoten.feed.comment/' || c.rkey) = n.subject_uri
49 where
50 n.recipient_did = ?
51 order by n.created_at desc
52 limit ? offset ?
53 `
54
55 rows, err := e.Query(query, did, limit, offset)
56 if err != nil {
57 return nil, fmt.Errorf("failed to query notifications: %w", err)
58 }
59 defer rows.Close()
60
61 var notifications []domain.Notification
62 for rows.Next() {
63 var notification domain.Notification
64 var createdAtStr string
65 var subjectURIStr string
66 var studySessionURIStr sql.NullString
67
68 err := rows.Scan(
69 ¬ification.ID,
70 ¬ification.RecipientDID,
71 ¬ification.ActorDID,
72 &subjectURIStr,
73 ¬ification.State,
74 ¬ification.Type,
75 &createdAtStr,
76 &studySessionURIStr,
77 )
78 if err != nil {
79 return nil, fmt.Errorf("failed to scan notification row: %w", err)
80 }
81
82 createdAt, err := time.Parse(time.RFC3339, createdAtStr)
83 if err != nil {
84 return nil, fmt.Errorf("failed to parse createdAt string '%s': %w", createdAtStr, err)
85 }
86 notification.CreatedAt = createdAt
87
88 subjectURI, err := syntax.ParseATURI(subjectURIStr)
89 if err != nil {
90 return nil, fmt.Errorf("failed to parse at-uri: %w", err)
91 }
92 notification.SubjectRKey = subjectURI.RecordKey().String()
93
94 subjectDID, err := subjectURI.Authority().AsDID()
95 if err != nil {
96 return nil, fmt.Errorf("failed to identify subject did: %w", err)
97 }
98 notification.SubjectDID = subjectDID.String()
99
100 if studySessionURIStr.Valid {
101 ssURI, err := syntax.ParseATURI(studySessionURIStr.String)
102 if err != nil {
103 return nil, fmt.Errorf("failed to parse study session at-uri: %w", err)
104 }
105 notification.StudySessionURI = &ssURI
106 }
107
108 notifications = append(notifications, notification)
109 }
110 if err = rows.Err(); err != nil {
111 return nil, err
112 }
113
114 return notifications, nil
115}
116
117func UnreadNotificationCount(e Execer, recipientDID string) (int, error) {
118 query := `select count(*) from notifications where recipient_did = ? and state = 0;`
119
120 var count int
121 row := e.QueryRow(query, recipientDID)
122 if err := row.Scan(&count); err != nil {
123 return 0, fmt.Errorf("failed to get unread notification count: %w", err)
124 }
125
126 return count, nil
127}
128
129func MarkNotificationsAsRead(e Execer, did string) error {
130 query := `
131 update notifications
132 set state = 1
133 where recipient_did = ? and state = 0;
134 `
135
136 _, err := e.Exec(query, did)
137 if err != nil {
138 return fmt.Errorf("failed to mark notifications as read for did %s: %w", did, err)
139 }
140
141 return nil
142}