From faac7e9b160ae198b093f4b1ee4860d6987c4465 Mon Sep 17 00:00:00 2001 From: Alessio Date: Wed, 6 Nov 2024 21:54:21 -0800 Subject: [PATCH] Add marking notifications as read --- cmd/twitter/main.go | 11 ++++++++++- internal/webserver/handler_notifications.go | 21 +++++++++++++++++++++ internal/webserver/server.go | 2 +- internal/webserver/tpl/notifications.tpl | 17 ++++++++++++++--- pkg/scraper/api_types.go | 11 +++++++++++ pkg/scraper/api_types_notifications.go | 19 +++++++++++++++++++ pkg/scraper/api_types_notifications_test.go | 3 +++ 7 files changed, 79 insertions(+), 5 deletions(-) diff --git a/cmd/twitter/main.go b/cmd/twitter/main.go index 5b23a91..b996c7a 100644 --- a/cmd/twitter/main.go +++ b/cmd/twitter/main.go @@ -83,7 +83,7 @@ func main() { if len(args) < 2 { if len(args) == 1 && (args[0] == "webserver" || args[0] == "fetch_timeline" || args[0] == "fetch_timeline_following_only" || args[0] == "fetch_inbox" || args[0] == "get_bookmarks" || - args[0] == "get_notifications") { + args[0] == "get_notifications" || args[0] == "mark_notifications_as_read") { // Doesn't need a target, so create a fake second arg args = append(args, "") } else { @@ -195,6 +195,8 @@ func main() { fetch_timeline(true) case "get_notifications": get_notifications(*how_many) + case "mark_notifications_as_read": + mark_notification_as_read() case "download_tweet_content": download_tweet_content(target) case "search": @@ -664,3 +666,10 @@ func get_notifications(how_many int) { len(trove.Notifications), len(trove.Tweets), len(trove.Users), ), nil) } + +func mark_notification_as_read() { + if err := api.MarkNotificationsAsRead(); err != nil { + panic(err) + } + happy_exit("Notifications marked as read", nil) +} diff --git a/internal/webserver/handler_notifications.go b/internal/webserver/handler_notifications.go index 32f45a2..1a2c3e0 100644 --- a/internal/webserver/handler_notifications.go +++ b/internal/webserver/handler_notifications.go @@ -3,9 +3,17 @@ package webserver import ( "net/http" "strconv" + "strings" ) func (app *Application) Notifications(w http.ResponseWriter, r *http.Request) { + app.traceLog.Printf("'Notifications' handler (path: %q)", r.URL.Path) + parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") + if parts[0] == "mark-all-as-read" { + app.NotificationsMarkAsRead(w, r) + return + } + cursor_val := 0 cursor_param := r.URL.Query().Get("cursor") if cursor_param != "" { @@ -26,3 +34,16 @@ func (app *Application) Notifications(w http.ResponseWriter, r *http.Request) { app.buffered_render_page(w, "tpl/notifications.tpl", PageGlobalData{TweetTrove: feed.TweetTrove}, feed) } } + +func (app *Application) NotificationsMarkAsRead(w http.ResponseWriter, r *http.Request) { + err := app.API.MarkNotificationsAsRead() + if err != nil { + panic(err) + } + app.toast(w, r, Toast{ + Title: "Success", + Message: `Notifications marked as "read"`, + Type: "success", + AutoCloseDelay: 2000, + }) +} diff --git a/internal/webserver/server.go b/internal/webserver/server.go index 2fb2a32..d41cb6f 100644 --- a/internal/webserver/server.go +++ b/internal/webserver/server.go @@ -134,7 +134,7 @@ func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) { case "bookmarks": app.Bookmarks(w, r) case "notifications": - app.Notifications(w, r) + http.StripPrefix("/notifications", http.HandlerFunc(app.Notifications)).ServeHTTP(w, r) case "messages": http.StripPrefix("/messages", http.HandlerFunc(app.Messages)).ServeHTTP(w, r) case "nav-sidebar-poll-updates": diff --git a/internal/webserver/tpl/notifications.tpl b/internal/webserver/tpl/notifications.tpl index 1ef8664..b535e9e 100644 --- a/internal/webserver/tpl/notifications.tpl +++ b/internal/webserver/tpl/notifications.tpl @@ -6,17 +6,28 @@
{{/* Extra div to take up a slot in the `row` */}}

Notifications

+ + + - +
-
- {{template "timeline" .}} +
+
+
+
+ +
+
+
+ {{template "timeline" .}} +
{{end}} diff --git a/pkg/scraper/api_types.go b/pkg/scraper/api_types.go index 7c28439..1806e03 100644 --- a/pkg/scraper/api_types.go +++ b/pkg/scraper/api_types.go @@ -526,6 +526,17 @@ func (t *TweetResponse) GetCursor() string { return "" } +func (t *TweetResponse) GetCursorTop() string { + for _, instr := range t.Timeline.Instructions { + for _, entry := range instr.AddEntries.Entries { + if strings.Contains(entry.EntryID, "cursor-top") { + return entry.Content.Operation.Cursor.Value + } + } + } + return "" +} + /** * Test for one case of end-of-feed. Cursor increments on each request for some reason, but * there's no new content. This seems to happen when there's a pinned tweet. diff --git a/pkg/scraper/api_types_notifications.go b/pkg/scraper/api_types_notifications.go index 457586e..95a91af 100644 --- a/pkg/scraper/api_types_notifications.go +++ b/pkg/scraper/api_types_notifications.go @@ -63,6 +63,25 @@ func (api *API) GetNotifications(how_many int) (TweetTrove, int64, error) { return trove, resp.CheckUnreadNotifications(), nil } +func (api *API) MarkNotificationsAsRead() error { + resp, err := api.GetNotificationsPage("") + if err != nil { + return err + } + cursor := resp.GetCursorTop() + if cursor == "" { + panic(fmt.Sprintf("No top cursor found: \n%#v", resp)) + } + rslt := struct { + Cursor string `json:"cursor"` + }{} + api.do_http_POST("https://twitter.com/i/api/2/notifications/all/last_seen_cursor.json", "cursor=" + cursor, &rslt) + if rslt.Cursor == "" { + panic("got blank cursor back...?") + } + return nil +} + // Check a Notifications result for unread notifications. Returns `0` if there are none. func (t TweetResponse) CheckUnreadNotifications() int64 { for _, instr := range t.Timeline.Instructions { diff --git a/pkg/scraper/api_types_notifications_test.go b/pkg/scraper/api_types_notifications_test.go index 5a1cc87..aa10f8b 100644 --- a/pkg/scraper/api_types_notifications_test.go +++ b/pkg/scraper/api_types_notifications_test.go @@ -163,6 +163,9 @@ func TestParseNotificationsPage(t *testing.T) { bottom_cursor := resp.GetCursor() assert.Equal("DAACDAABCgABFKncQJGVgAQIAAIAAAABCAADSQ3bEQgABIsN6BEACwACAAAAC0FaRkxRSXFNLTJJAAA", bottom_cursor) assert.False(resp.IsEndOfFeed()) + + // Test cursor-top + assert.Equal(resp.GetCursorTop(), "DAABDAABCgABFKncQJGVgAQIAAIAAAABCAADSQ3bEQgABIsN6BEACwACAAAAC0FaR0lLcUhkUVhrAAA") } func TestParseNotificationsEndOfFeed(t *testing.T) {