This repository has no description
1package xrpc
2
3import (
4 "encoding/json"
5 "net/http"
6 "strconv"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 "tangled.org/core/api/tangled"
10 "tangled.org/core/appview/db"
11 "tangled.org/core/appview/models"
12 "tangled.org/core/appview/pagination"
13 "tangled.org/core/orm"
14 xrpcerr "tangled.org/core/xrpc/errors"
15)
16
17func (x *Xrpc) NotificationList(w http.ResponseWriter, r *http.Request) {
18 l := x.Logger.With("handler", "NotificationList")
19
20 did, ok := actorDid(r)
21 if !ok {
22 writeError(w, xrpcerr.MissingActorDidError, http.StatusForbidden)
23 return
24 }
25
26 q := r.URL.Query()
27 readFilter := q.Get("read")
28 categoryFilter := q.Get("category")
29
30 filters := []orm.Filter{orm.FilterEq("recipient_did", did)}
31 if readFilter == "unread" {
32 filters = append(filters, orm.FilterEq("read", 0))
33 }
34 switch categoryFilter {
35 case "social":
36 filters = append(filters, orm.FilterIn("type", models.SocialNotificationTypes))
37 case "work":
38 filters = append(filters, orm.FilterIn("type", models.WorkNotificationTypes))
39 }
40
41 limit := 50
42 if s := q.Get("limit"); s != "" {
43 if n, err := strconv.Atoi(s); err == nil && n > 0 && n <= 100 {
44 limit = n
45 }
46 }
47
48 notifications, err := db.GetNotificationsWithEntities(x.DB, pagination.Page{Limit: limit}, filters...)
49 if err != nil {
50 l.Error("failed to get notifications", "err", err)
51 writeError(w, errInternal, http.StatusInternalServerError)
52 return
53 }
54
55 unreadBase := []orm.Filter{
56 orm.FilterEq("recipient_did", did),
57 orm.FilterEq("read", 0),
58 }
59 workUnread, _ := db.CountNotifications(x.DB,
60 append(unreadBase, orm.FilterIn("type", models.WorkNotificationTypes))...)
61 socialUnread, _ := db.CountNotifications(x.DB,
62 append(unreadBase, orm.FilterIn("type", models.SocialNotificationTypes))...)
63
64 items := make([]*tangled.TempNotificationListNotifications_Notification, 0, len(notifications))
65 for _, n := range notifications {
66 item := &tangled.TempNotificationListNotifications_Notification{
67 Id: n.ID,
68 Type: string(n.Type),
69 Category: notificationCategory(n.Type),
70 ActorDid: n.ActorDid,
71 Read: n.Read,
72 CreatedAt: n.Created.UTC().Format("2006-01-02T15:04:05.000Z"),
73 }
74 if n.Repo != nil {
75 s := n.Repo.RepoDid
76 item.RepoDid = &s
77 }
78 if n.Issue != nil {
79 s := n.Issue.AtUri().String()
80 item.IssueAt = &s
81 }
82 if n.Pull != nil {
83 s := n.Pull.AtUri().String()
84 item.PullAt = &s
85 }
86 items = append(items, item)
87 }
88
89 x.writeJSON(w, &tangled.TempNotificationListNotifications_Output{
90 Notifications: items,
91 WorkUnreadCount: workUnread,
92 SocialUnreadCount: socialUnread,
93 })
94}
95
96func (x *Xrpc) NotificationGetUnreadCount(w http.ResponseWriter, r *http.Request) {
97 l := x.Logger.With("handler", "NotificationGetUnreadCount")
98
99 did, ok := actorDid(r)
100 if !ok {
101 writeError(w, xrpcerr.MissingActorDidError, http.StatusForbidden)
102 return
103 }
104
105 count, err := db.CountNotifications(x.DB,
106 orm.FilterEq("recipient_did", did),
107 orm.FilterEq("read", 0),
108 )
109 if err != nil {
110 l.Error("failed to count unread notifications", "err", err)
111 writeError(w, errInternal, http.StatusInternalServerError)
112 return
113 }
114
115 x.writeJSON(w, &tangled.TempNotificationGetUnreadCount_Output{Count: count})
116}
117
118func (x *Xrpc) NotificationUpdateSeen(w http.ResponseWriter, r *http.Request) {
119 l := x.Logger.With("handler", "NotificationUpdateSeen")
120
121 did, ok := actorDid(r)
122 if !ok {
123 writeError(w, xrpcerr.MissingActorDidError, http.StatusForbidden)
124 return
125 }
126
127 var input tangled.TempNotificationUpdateSeen_Input
128 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
129 writeError(w, errBadRequestBody, http.StatusBadRequest)
130 return
131 }
132
133 var err error
134 if input.Read {
135 err = db.MarkNotificationRead(x.DB, input.Id, did)
136 } else {
137 err = db.MarkNotificationUnread(x.DB, input.Id, did)
138 }
139 if err != nil {
140 l.Error("failed to update notification read state", "err", err)
141 writeError(w, errInternal, http.StatusInternalServerError)
142 return
143 }
144
145 w.WriteHeader(http.StatusOK)
146}
147
148func (x *Xrpc) NotificationMarkAllRead(w http.ResponseWriter, r *http.Request) {
149 l := x.Logger.With("handler", "NotificationMarkAllRead")
150
151 did, ok := actorDid(r)
152 if !ok {
153 writeError(w, xrpcerr.MissingActorDidError, http.StatusForbidden)
154 return
155 }
156
157 if err := db.MarkAllNotificationsRead(x.DB, did); err != nil {
158 l.Error("failed to mark all notifications read", "err", err)
159 writeError(w, errInternal, http.StatusInternalServerError)
160 return
161 }
162
163 w.WriteHeader(http.StatusOK)
164}
165
166func (x *Xrpc) NotificationDelete(w http.ResponseWriter, r *http.Request) {
167 l := x.Logger.With("handler", "NotificationDelete")
168
169 did, ok := actorDid(r)
170 if !ok {
171 writeError(w, xrpcerr.MissingActorDidError, http.StatusForbidden)
172 return
173 }
174
175 var input tangled.TempNotificationDeleteNotification_Input
176 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
177 writeError(w, errBadRequestBody, http.StatusBadRequest)
178 return
179 }
180
181 if err := db.DeleteNotification(x.DB, input.Id, did); err != nil {
182 l.Error("failed to delete notification", "err", err)
183 writeError(w, errInternal, http.StatusInternalServerError)
184 return
185 }
186
187 w.WriteHeader(http.StatusOK)
188}
189
190func (x *Xrpc) NotificationGetPreferences(w http.ResponseWriter, r *http.Request) {
191 l := x.Logger.With("handler", "NotificationGetPreferences")
192
193 did, ok := actorDid(r)
194 if !ok {
195 writeError(w, xrpcerr.MissingActorDidError, http.StatusForbidden)
196 return
197 }
198
199 prefs, err := db.GetNotificationPreference(x.DB, did)
200 if err != nil {
201 l.Error("failed to get notification preferences", "err", err)
202 writeError(w, errInternal, http.StatusInternalServerError)
203 return
204 }
205
206 x.writeJSON(w, &tangled.TempNotificationGetPreferences_Preferences{
207 EmailNotifications: prefs.EmailNotifications,
208 Followed: prefs.Followed,
209 IssueClosed: prefs.IssueClosed,
210 IssueCommented: prefs.IssueCommented,
211 IssueCreated: prefs.IssueCreated,
212 PullCommented: prefs.PullCommented,
213 PullCreated: prefs.PullCreated,
214 PullMerged: prefs.PullMerged,
215 RepoStarred: prefs.RepoStarred,
216 UserMentioned: prefs.UserMentioned,
217 })
218}
219
220func (x *Xrpc) NotificationUpdatePreferences(w http.ResponseWriter, r *http.Request) {
221 l := x.Logger.With("handler", "NotificationUpdatePreferences")
222
223 did, ok := actorDid(r)
224 if !ok {
225 writeError(w, xrpcerr.MissingActorDidError, http.StatusForbidden)
226 return
227 }
228
229 var input tangled.TempNotificationUpdatePreferences_Input
230 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
231 writeError(w, errBadRequestBody, http.StatusBadRequest)
232 return
233 }
234
235 existing, err := db.GetNotificationPreference(x.DB, did)
236 if err != nil {
237 l.Error("failed to get existing notification preferences", "err", err)
238 writeError(w, errInternal, http.StatusInternalServerError)
239 return
240 }
241
242 prefs := &models.NotificationPreferences{
243 UserDid: syntax.DID(did),
244 RepoStarred: applyBoolPtr(existing.RepoStarred, input.RepoStarred),
245 IssueCreated: applyBoolPtr(existing.IssueCreated, input.IssueCreated),
246 IssueCommented: applyBoolPtr(existing.IssueCommented, input.IssueCommented),
247 IssueClosed: applyBoolPtr(existing.IssueClosed, input.IssueClosed),
248 PullCreated: applyBoolPtr(existing.PullCreated, input.PullCreated),
249 PullCommented: applyBoolPtr(existing.PullCommented, input.PullCommented),
250 PullMerged: applyBoolPtr(existing.PullMerged, input.PullMerged),
251 Followed: applyBoolPtr(existing.Followed, input.Followed),
252 UserMentioned: applyBoolPtr(existing.UserMentioned, input.UserMentioned),
253 EmailNotifications: applyBoolPtr(existing.EmailNotifications, input.EmailNotifications),
254 }
255
256 if err := x.DB.UpdateNotificationPreferences(r.Context(), prefs); err != nil {
257 l.Error("failed to update notification preferences", "err", err)
258 writeError(w, errInternal, http.StatusInternalServerError)
259 return
260 }
261
262 w.WriteHeader(http.StatusOK)
263}
264
265func notificationCategory(t models.NotificationType) string {
266 for _, st := range models.SocialNotificationTypes {
267 if st == t {
268 return "social"
269 }
270 }
271 return "work"
272}
273
274func applyBoolPtr(existing bool, update *bool) bool {
275 if update != nil {
276 return *update
277 }
278 return existing
279}