+1
main.go
+1
main.go
···
131
131
db.AutoMigrate(StarterPack{})
132
132
db.AutoMigrate(backend.SyncInfo{})
133
133
db.AutoMigrate(Notification{})
134
+
db.AutoMigrate(NotificationSeen{})
134
135
db.AutoMigrate(SequenceTracker{})
135
136
db.Exec("CREATE INDEX IF NOT EXISTS reposts_subject_idx ON reposts (subject)")
136
137
db.Exec("CREATE INDEX IF NOT EXISTS posts_reply_to_idx ON posts (reply_to)")
+5
models/models.go
+5
models/models.go
+80
-11
xrpc/notification/listNotifications.go
+80
-11
xrpc/notification/listNotifications.go
···
13
13
lexutil "github.com/bluesky-social/indigo/lex/util"
14
14
"github.com/labstack/echo/v4"
15
15
"github.com/whyrusleeping/konbini/hydration"
16
+
models "github.com/whyrusleeping/konbini/models"
16
17
"github.com/whyrusleeping/konbini/views"
17
-
"github.com/whyrusleeping/market/models"
18
18
"gorm.io/gorm"
19
+
"gorm.io/gorm/clause"
19
20
)
20
21
21
22
// HandleListNotifications implements app.bsky.notification.listNotifications
22
23
func HandleListNotifications(c echo.Context, db *gorm.DB, hydrator *hydration.Hydrator) error {
23
24
viewer := getUserDID(c)
24
25
if viewer == "" {
25
-
return c.JSON(http.StatusUnauthorized, map[string]interface{}{
26
+
return c.JSON(http.StatusUnauthorized, map[string]any{
26
27
"error": "AuthenticationRequired",
27
28
"message": "authentication required",
28
29
})
···
77
78
}
78
79
query += ` ORDER BY n.created_at DESC LIMIT ?`
79
80
80
-
var queryArgs []interface{}
81
+
var queryArgs []any
81
82
queryArgs = append(queryArgs, viewer)
82
83
if cursor > 0 {
83
84
queryArgs = append(queryArgs, cursor)
···
85
86
queryArgs = append(queryArgs, limit)
86
87
87
88
if err := db.Raw(query, queryArgs...).Scan(&rows).Error; err != nil {
88
-
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
89
+
return c.JSON(http.StatusInternalServerError, map[string]any{
89
90
"error": "InternalError",
90
91
"message": "failed to query notifications",
91
92
})
···
142
143
func HandleGetUnreadCount(c echo.Context, db *gorm.DB, hydrator *hydration.Hydrator) error {
143
144
viewer := getUserDID(c)
144
145
if viewer == "" {
145
-
return c.JSON(http.StatusUnauthorized, map[string]interface{}{
146
+
return c.JSON(http.StatusUnauthorized, map[string]any{
146
147
"error": "AuthenticationRequired",
147
148
"message": "authentication required",
148
149
})
149
150
}
150
151
151
-
// For now, return 0 - we'd need to track read state in the database
152
-
return c.JSON(http.StatusOK, map[string]interface{}{
153
-
"count": 0,
152
+
var repo models.Repo
153
+
if err := db.Find(&repo, "did = ?", viewer).Error; err != nil {
154
+
return err
155
+
}
156
+
157
+
var lastSeen time.Time
158
+
if err := db.Raw("SELECT seen_at FROM notification_seens WHERE repo = ?", repo.ID).Scan(&lastSeen).Error; err != nil {
159
+
return err
160
+
}
161
+
162
+
var count int
163
+
query := `SELECT count(*) FROM notifications WHERE created_at > ? AND for = ?`
164
+
if err := db.Raw(query, lastSeen, repo.ID).Scan(&count).Error; err != nil {
165
+
return c.JSON(http.StatusInternalServerError, map[string]any{
166
+
"error": "InternalError",
167
+
"message": "failed to count unread notifications",
168
+
})
169
+
}
170
+
171
+
return c.JSON(http.StatusOK, map[string]any{
172
+
"count": count,
154
173
})
155
174
}
156
175
···
158
177
func HandleUpdateSeen(c echo.Context, db *gorm.DB, hydrator *hydration.Hydrator) error {
159
178
viewer := getUserDID(c)
160
179
if viewer == "" {
161
-
return c.JSON(http.StatusUnauthorized, map[string]interface{}{
180
+
return c.JSON(http.StatusUnauthorized, map[string]any{
162
181
"error": "AuthenticationRequired",
163
182
"message": "authentication required",
164
183
})
165
184
}
166
185
167
-
// For now, just return success - we'd need to track seen timestamps in the database
168
-
return c.JSON(http.StatusOK, map[string]interface{}{})
186
+
var body bsky.NotificationUpdateSeen_Input
187
+
if err := c.Bind(&body); err != nil {
188
+
return c.JSON(http.StatusBadRequest, map[string]any{
189
+
"error": "InvalidRequest",
190
+
"message": "invalid request body",
191
+
})
192
+
}
193
+
194
+
// Parse the seenAt timestamp
195
+
seenAt, err := time.Parse(time.RFC3339, body.SeenAt)
196
+
if err != nil {
197
+
return c.JSON(http.StatusBadRequest, map[string]any{
198
+
"error": "InvalidRequest",
199
+
"message": "invalid seenAt timestamp",
200
+
})
201
+
}
202
+
203
+
// Get the viewer's repo ID
204
+
var repoID uint
205
+
if err := db.Raw("SELECT id FROM repos WHERE did = ?", viewer).Scan(&repoID).Error; err != nil {
206
+
return c.JSON(http.StatusInternalServerError, map[string]any{
207
+
"error": "InternalError",
208
+
"message": "failed to find viewer repo",
209
+
})
210
+
}
211
+
212
+
if repoID == 0 {
213
+
return c.JSON(http.StatusInternalServerError, map[string]any{
214
+
"error": "InternalError",
215
+
"message": "viewer repo not found",
216
+
})
217
+
}
218
+
219
+
// Upsert the NotificationSeen record
220
+
notifSeen := models.NotificationSeen{
221
+
Repo: repoID,
222
+
SeenAt: seenAt,
223
+
}
224
+
225
+
err = db.Clauses(clause.OnConflict{
226
+
Columns: []clause.Column{{Name: "repo"}},
227
+
DoUpdates: clause.AssignmentColumns([]string{"seen_at"}),
228
+
}).Create(¬ifSeen).Error
229
+
230
+
if err != nil {
231
+
return c.JSON(http.StatusInternalServerError, map[string]any{
232
+
"error": "InternalError",
233
+
"message": "failed to update seen timestamp",
234
+
})
235
+
}
236
+
237
+
return c.JSON(http.StatusOK, map[string]any{})
169
238
}
170
239
171
240
func getUserDID(c echo.Context) string {