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
This commit is contained in:
Alessio 2023-11-05 15:27:40 -04:00
parent fa9c913b53
commit 694a8e0bc5
7 changed files with 117 additions and 79 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mdi-refresh" viewBox="0 0 24 24"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" /></svg>

After

Width:  |  Height:  |  Size: 306 B

View File

@ -3,7 +3,7 @@
{{$author := (user $main_tweet.UserID)}}
<div class="tweet"
{{if (not (eq $main_tweet.ID (focused_tweet_id)))}}
hx-post="/tweet/{{$main_tweet.ID}}"
hx-get="/tweet/{{$main_tweet.ID}}"
hx-trigger="click"
hx-target="body"
hx-swap="outerHTML show:#focused-tweet:top"
@ -130,7 +130,7 @@
<span>Open on twitter.com</span>
</li>
</a>
<a class="unstyled-link" target="_blank" hx-post="/tweet/{{$main_tweet.ID}}/scrape" hx-target="body">
<a class="unstyled-link" target="_blank" hx-get="/tweet/{{$main_tweet.ID}}?scrape" hx-target="body">
<li class="quick-link">
<img class="svg-icon" src="/static/icons/download.svg" />
<span>Re-fetch tweet</span>

View File

@ -42,30 +42,18 @@
<span class="following-count">{{$user.FollowingCount}}</span>
</div>
<div class="dropdown" hx-trigger="click consume">
<button class="dropdown-button" title="Options">
<img class="svg-icon" src="/static/icons/more.svg" />
</button>
<ul class="dropdown-items">
<a class="unstyled-link" target="_blank" href="https://twitter.com/{{$user.Handle}}">
<li class="quick-link">
<img class="svg-icon" src="/static/icons/external-link.svg" />
<span>Open on twitter.com</span>
</li>
</a>
<a class="unstyled-link" target="_blank" hx-post="/{{$user.Handle}}/scrape" hx-target="body">
<li class="quick-link">
<img class="svg-icon" src="/static/icons/download.svg" />
<span>Re-fetch user feed</span>
</li>
</a>
<a class="unstyled-link" target="_blank" hx-post="/{{$user.Handle}}/likes/scrape" hx-target="body">
<li class="quick-link">
<img class="svg-icon" src="/static/icons/download.svg" />
<span>Re-fetch user likes</span>
</li>
</a>
</ul>
<a class="unstyled-link" target="_blank" href="https://twitter.com/{{$user.Handle}}">
<li class="quick-link">
<img class="svg-icon" src="/static/icons/external-link.svg" />
<span>Open on twitter.com</span>
</li>
</a>
<div class="XXX">
<a class="unstyled-link" title="Refresh" hx-get="?scrape" hx-target="body">
<li class="quick-link">
<img class="svg-icon" src="/static/icons/refresh.svg" />
</li>
</a>
</div>
</div>
</div>

View File

@ -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" {