This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / xrpc / notifications.go
8.2 kB 279 lines
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}