From 694a8e0bc5c5eeae7d50337e07c45d28fb232f94 Mon Sep 17 00:00:00 2001 From: Alessio Date: Sun, 5 Nov 2023 15:27:40 -0400 Subject: [PATCH] Change "scrape" from a URL param to a query param - create generic "refresh" button for User Feed that refreshes whichever tab you're on - refactor TweetDetail view into multiple pieces for easier maintenance - on "Already liked this tweet" error, save the Like instead of discarding it --- internal/webserver/handler_tweet_detail.go | 129 +++++++++++------- internal/webserver/handler_user_feed.go | 7 +- internal/webserver/response_helpers.go | 13 ++ internal/webserver/static/icons/refresh.svg | 1 + .../tpl/tweet_page_includes/single_tweet.tpl | 4 +- internal/webserver/tpl/user_feed.tpl | 36 ++--- pkg/scraper/api_types_posting.go | 6 +- 7 files changed, 117 insertions(+), 79 deletions(-) create mode 100644 internal/webserver/static/icons/refresh.svg diff --git a/internal/webserver/handler_tweet_detail.go b/internal/webserver/handler_tweet_detail.go index eccbc23..cb24379 100644 --- a/internal/webserver/handler_tweet_detail.go +++ b/internal/webserver/handler_tweet_detail.go @@ -11,6 +11,8 @@ import ( "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" ) +var ErrNotFound = errors.New("not found") + type TweetDetailData struct { persistence.TweetDetailView MainTweetID scraper.TweetID @@ -37,6 +39,77 @@ func (t TweetDetailData) FocusedTweetID() scraper.TweetID { return t.MainTweetID } +func (app *Application) ensure_tweet(id scraper.TweetID, is_forced bool, is_conversation_required bool) (scraper.Tweet, error) { + is_available := false + is_needing_scrape := is_forced + + // Check if tweet is already in DB + tweet, err := app.Profile.GetTweetById(id) + if err != nil { + if errors.Is(err, persistence.ErrNotInDB) { + is_needing_scrape = true + is_available = false + } else { + panic(err) + } + } else { + is_available = true + if !tweet.IsConversationScraped { + is_needing_scrape = true + } + } + if is_available && !is_conversation_required { // TODO: get rid of this, just force the fetch in subsequent handlers if needed + is_needing_scrape = false + } + + if is_needing_scrape && !app.IsScrapingDisabled { + trove, err := scraper.GetTweetFullAPIV2(id, 50) // TODO: parameterizable + if err == nil { + app.Profile.SaveTweetTrove(trove) + is_available = true + } else { + app.ErrorLog.Print(err) + // TODO: show error in UI + } + } else if is_needing_scrape { + app.InfoLog.Printf("Would have scraped Tweet: %d", id) + } + + if !is_available { + return scraper.Tweet{}, ErrNotFound + } + return tweet, nil +} + +func (app *Application) LikeTweet(w http.ResponseWriter, r *http.Request) { + tweet := get_tweet_from_context(r.Context()) + like, err := scraper.LikeTweet(tweet.ID) + // "Already Liked This Tweet" is no big deal-- we can just update the UI as if it succeeded + if err != nil && !errors.Is(err, scraper.AlreadyLikedThisTweet) { + // It's a different error + panic(err) + } + err = app.Profile.SaveLike(like) + panic_if(err) + tweet.IsLikedByCurrentUser = true + + app.buffered_render_basic_htmx(w, "likes-count", tweet) +} +func (app *Application) UnlikeTweet(w http.ResponseWriter, r *http.Request) { + tweet := get_tweet_from_context(r.Context()) + err := scraper.UnlikeTweet(tweet.ID) + // As above, "Haven't Liked This Tweet" is no big deal-- we can just update the UI as if the request succeeded + if err != nil && !errors.Is(err, scraper.HaventLikedThisTweet) { + // It's a different error + panic(err) + } + err = app.Profile.DeleteLike(scraper.Like{UserID: app.ActiveUser.ID, TweetID: tweet.ID}) + panic_if(err) + tweet.IsLikedByCurrentUser = false + + app.buffered_render_basic_htmx(w, "likes-count", tweet) +} + func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) { app.traceLog.Printf("'TweetDetail' handler (path: %q)", r.URL.Path) @@ -51,63 +124,21 @@ func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) { data := NewTweetDetailData() data.MainTweetID = tweet_id - is_needing_scrape := (len(parts) > 2 && parts[2] == "scrape") - is_available := false + is_scrape_required := r.URL.Query().Has("scrape") + is_conversation_required := len(parts) <= 2 || (parts[2] != "like" && parts[2] != "unlike") - // Check if tweet is already in DB - tweet, err := app.Profile.GetTweetById(tweet_id) - if err != nil { - if errors.Is(err, persistence.ErrNotInDB) { - is_needing_scrape = true - is_available = false - } else { - panic(err) - } - } else { - is_available = true - if !tweet.IsConversationScraped { - is_needing_scrape = true - } - } - if is_available && len(parts) > 2 && (parts[2] == "like" || parts[2] == "unlike") { - is_needing_scrape = false - } - - if is_needing_scrape && !app.IsScrapingDisabled { - trove, err := scraper.GetTweetFullAPIV2(tweet_id, 50) // TODO: parameterizable - if err == nil { - app.Profile.SaveTweetTrove(trove) - is_available = true - } else { - app.ErrorLog.Print(err) - // TODO: show error in UI - } - } - - if !is_available { + tweet, err := app.ensure_tweet(tweet_id, is_scrape_required, is_conversation_required) + if errors.Is(err, ErrNotFound) { app.error_404(w) return } + req_with_tweet := r.WithContext(add_tweet_to_context(r.Context(), tweet)) if len(parts) > 2 && parts[2] == "like" { - like, err := scraper.LikeTweet(tweet.ID) - // if err != nil && !errors.Is(err, scraper.AlreadyLikedThisTweet) {} - panic_if(err) - fmt.Printf("Like: %#v\n", like) - err = app.Profile.SaveLike(like) - panic_if(err) - tweet.IsLikedByCurrentUser = true - - app.buffered_render_basic_htmx(w, "likes-count", tweet) + app.LikeTweet(w, req_with_tweet) return } else if len(parts) > 2 && parts[2] == "unlike" { - err = scraper.UnlikeTweet(tweet_id) - panic_if(err) - err = app.Profile.DeleteLike(scraper.Like{UserID: app.ActiveUser.ID, TweetID: tweet.ID}) - panic_if(err) - tweet.IsLikedByCurrentUser = false - - app.buffered_render_basic_htmx(w, "likes-count", tweet) + app.UnlikeTweet(w, req_with_tweet) return } diff --git a/internal/webserver/handler_user_feed.go b/internal/webserver/handler_user_feed.go index 89dd1fa..c8fb34b 100644 --- a/internal/webserver/handler_user_feed.go +++ b/internal/webserver/handler_user_feed.go @@ -42,13 +42,14 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) { return } - if len(parts) > 1 && parts[len(parts)-1] == "scrape" { + if r.URL.Query().Has("scrape") { if app.IsScrapingDisabled { + app.InfoLog.Printf("Would have scraped: %s", r.URL.Path) http.Error(w, "Scraping is disabled (are you logged in?)", 401) return } - if len(parts) == 2 { // Already checked the last part is "scrape" + if len(parts) == 1 { // The URL is just the user handle // Run scraper trove, err := scraper.GetUserFeedGraphqlFor(user.ID, 50) // TODO: parameterizable if err != nil { @@ -56,7 +57,7 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) { // TOOD: show error in UI } app.Profile.SaveTweetTrove(trove) - } else if len(parts) == 3 && parts[1] == "likes" { + } else if len(parts) == 2 && parts[1] == "likes" { trove, err := scraper.GetUserLikes(user.ID, 50) // TODO: parameterizable if err != nil { app.ErrorLog.Print(err) diff --git a/internal/webserver/response_helpers.go b/internal/webserver/response_helpers.go index 7f474a7..f790815 100644 --- a/internal/webserver/response_helpers.go +++ b/internal/webserver/response_helpers.go @@ -2,6 +2,7 @@ package webserver import ( "bytes" + "context" "fmt" "html/template" "io" @@ -244,3 +245,15 @@ func cursor_to_query_params(c persistence.Cursor) string { result.Set("sort-order", c.SortOrder.String()) return result.Encode() } + +func add_tweet_to_context(ctx context.Context, tweet scraper.Tweet) context.Context { + return context.WithValue(ctx, "tweet", tweet) +} + +func get_tweet_from_context(ctx context.Context) scraper.Tweet { + tweet, is_ok := ctx.Value("tweet").(scraper.Tweet) + if !is_ok { + panic("Tweet not found in context") + } + return tweet +} diff --git a/internal/webserver/static/icons/refresh.svg b/internal/webserver/static/icons/refresh.svg new file mode 100644 index 0000000..57eb75d --- /dev/null +++ b/internal/webserver/static/icons/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/webserver/tpl/tweet_page_includes/single_tweet.tpl b/internal/webserver/tpl/tweet_page_includes/single_tweet.tpl index 327b244..076f8a8 100644 --- a/internal/webserver/tpl/tweet_page_includes/single_tweet.tpl +++ b/internal/webserver/tpl/tweet_page_includes/single_tweet.tpl @@ -3,7 +3,7 @@ {{$author := (user $main_tweet.UserID)}}
Open on twitter.com - +
- diff --git a/pkg/scraper/api_types_posting.go b/pkg/scraper/api_types_posting.go index 536565f..5e1c110 100644 --- a/pkg/scraper/api_types_posting.go +++ b/pkg/scraper/api_types_posting.go @@ -34,7 +34,11 @@ func (api API) LikeTweet(id TweetID) (Like, error) { } if len(result.Errors) > 0 { if strings.Contains(result.Errors[0].Message, "has already favorited tweet") { - return Like{}, AlreadyLikedThisTweet + return Like{ + UserID: api.UserID, + TweetID: id, + SortID: -1, + }, AlreadyLikedThisTweet } } if result.Data.FavoriteTweet != "Done" {