Add "with replies", "without replies", "media" and "likes" tab to User Feed
- Also add UI option to scrape a user's Likes from their user profile
This commit is contained in:
parent
d1edcbf363
commit
72df452401
@ -12,6 +12,7 @@ import (
|
||||
type UserProfileData struct {
|
||||
persistence.Feed
|
||||
scraper.UserID
|
||||
FeedType string
|
||||
}
|
||||
|
||||
func (t UserProfileData) Tweet(id scraper.TweetID) scraper.Tweet {
|
||||
@ -41,22 +42,42 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) == 2 && parts[1] == "scrape" {
|
||||
if len(parts) > 1 && parts[len(parts)-1] == "scrape" {
|
||||
if app.IsScrapingDisabled {
|
||||
http.Error(w, "Scraping is disabled (are you logged in?)", 401)
|
||||
return
|
||||
}
|
||||
|
||||
// Run scraper
|
||||
trove, err := scraper.GetUserFeedGraphqlFor(user.ID, 50) // TODO: parameterizable
|
||||
if err != nil {
|
||||
app.ErrorLog.Print(err)
|
||||
// TOOD: show error in UI
|
||||
if len(parts) == 2 { // Already checked the last part is "scrape"
|
||||
// Run scraper
|
||||
trove, err := scraper.GetUserFeedGraphqlFor(user.ID, 50) // TODO: parameterizable
|
||||
if err != nil {
|
||||
app.ErrorLog.Print(err)
|
||||
// TOOD: show error in UI
|
||||
}
|
||||
app.Profile.SaveTweetTrove(trove)
|
||||
} else if len(parts) == 3 && parts[1] == "likes" {
|
||||
trove, err := scraper.GetUserLikes(user.ID, 50) // TODO: parameterizable
|
||||
if err != nil {
|
||||
app.ErrorLog.Print(err)
|
||||
// TOOD: show error in UI
|
||||
}
|
||||
app.Profile.SaveTweetTrove(trove)
|
||||
}
|
||||
app.Profile.SaveTweetTrove(trove)
|
||||
}
|
||||
|
||||
c := persistence.NewUserFeedCursor(user.Handle)
|
||||
var c persistence.Cursor
|
||||
if len(parts) > 1 && parts[1] == "likes" {
|
||||
c = persistence.NewUserFeedLikesCursor(user.Handle)
|
||||
} else {
|
||||
c = persistence.NewUserFeedCursor(user.Handle)
|
||||
}
|
||||
if len(parts) > 1 && parts[1] == "without_replies" {
|
||||
c.FilterReplies = persistence.EXCLUDE
|
||||
}
|
||||
if len(parts) > 1 && parts[1] == "media" {
|
||||
c.FilterMedia = persistence.REQUIRE
|
||||
}
|
||||
err = parse_cursor_value(&c, r)
|
||||
if err != nil {
|
||||
app.error_400_with_message(w, "invalid cursor (must be a number)")
|
||||
@ -74,6 +95,11 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
|
||||
feed.Users[user.ID] = user
|
||||
|
||||
data := UserProfileData{Feed: feed, UserID: user.ID}
|
||||
if len(parts) == 2 {
|
||||
data.FeedType = parts[1]
|
||||
} else {
|
||||
data.FeedType = ""
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE {
|
||||
// It's a Show More request
|
||||
|
@ -133,6 +133,54 @@ func TestUserFeedWithCursorBadNumber(t *testing.T) {
|
||||
require.Equal(resp.StatusCode, 400)
|
||||
}
|
||||
|
||||
func TestUserFeedTweetsOnlyTab(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
resp := do_request(httptest.NewRequest("GET", "/Peter_Nimitz/without_replies", nil))
|
||||
require.Equal(resp.StatusCode, 200)
|
||||
|
||||
root, err := html.Parse(resp.Body)
|
||||
require.NoError(err)
|
||||
tweets := cascadia.QueryAll(root, selector(".timeline > .tweet"))
|
||||
assert.Len(tweets, 2)
|
||||
}
|
||||
|
||||
func TestUserFeedMediaTab(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
resp := do_request(httptest.NewRequest("GET", "/Cernovich/media", nil))
|
||||
require.Equal(resp.StatusCode, 200)
|
||||
|
||||
root, err := html.Parse(resp.Body)
|
||||
require.NoError(err)
|
||||
tweets := cascadia.QueryAll(root, selector(".timeline > .tweet"))
|
||||
assert.Len(tweets, 1)
|
||||
}
|
||||
|
||||
func TestUserFeedLikesTab(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
resp := do_request(httptest.NewRequest("GET", "/MysteryGrove/likes", nil))
|
||||
require.Equal(resp.StatusCode, 200)
|
||||
|
||||
root, err := html.Parse(resp.Body)
|
||||
require.NoError(err)
|
||||
tweets := cascadia.QueryAll(root, selector(".timeline > .tweet"))
|
||||
assert.Len(tweets, 5)
|
||||
|
||||
// Double check pagination works properly
|
||||
resp = do_request(httptest.NewRequest("GET", "/MysteryGrove/likes?cursor=5", nil))
|
||||
require.Equal(resp.StatusCode, 200)
|
||||
|
||||
root, err = html.Parse(resp.Body)
|
||||
require.NoError(err)
|
||||
tweets = cascadia.QueryAll(root, selector(".timeline > .tweet"))
|
||||
assert.Len(tweets, 4)
|
||||
}
|
||||
|
||||
// Timeline page
|
||||
// -------------
|
||||
|
||||
|
@ -221,6 +221,21 @@ h3 {
|
||||
.user-feed-header .profile-image {
|
||||
width: 8em;
|
||||
}
|
||||
.user-feed-header a.user-feed-tab {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
color: var(--color-twitter-text-gray);
|
||||
padding: 0.8em;
|
||||
}
|
||||
.user-feed-header a.user-feed-tab.active-tab {
|
||||
color: var(--color-twitter-blue);
|
||||
border-bottom: 0.2em solid var(--color-twitter-blue);
|
||||
}
|
||||
.user-feed-header a.user-feed-tab:hover {
|
||||
color: var(--color-twitter-blue);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
|
@ -59,10 +59,31 @@
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row user-feed-tabs-container">
|
||||
<a class="user-feed-tab unstyled-link {{if (eq .FeedType "")}}active-tab{{end}}" href="/{{$user.Handle}}">
|
||||
<span class="user-feed-tab-inner">Tweets and replies</span>
|
||||
</a>
|
||||
<a class="user-feed-tab unstyled-link {{if (eq .FeedType "without_replies")}}active-tab{{end}}" href="/{{$user.Handle}}/without_replies">
|
||||
<span class="user-feed-tab-inner">Tweets</span>
|
||||
</a>
|
||||
<a class="user-feed-tab unstyled-link {{if (eq .FeedType "media")}}active-tab{{end}}" href="/{{$user.Handle}}/media">
|
||||
<span class="user-feed-tab-inner">Media</span>
|
||||
</a>
|
||||
<a class="user-feed-tab unstyled-link {{if (eq .FeedType "likes")}}active-tab{{end}}" href="/{{$user.Handle}}/likes">
|
||||
<span class="user-feed-tab-inner">Likes</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline user-feed-timeline">
|
||||
|
@ -180,6 +180,23 @@ func NewUserFeedCursor(h scraper.UserHandle) Cursor {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a cursor appropriate for a user's Media tab
|
||||
func NewUserFeedMediaCursor(h scraper.UserHandle) Cursor {
|
||||
return Cursor{
|
||||
Keywords: []string{},
|
||||
ToUserHandles: []scraper.UserHandle{},
|
||||
SinceTimestamp: scraper.TimestampFromUnix(0),
|
||||
UntilTimestamp: scraper.TimestampFromUnix(0),
|
||||
CursorPosition: CURSOR_START,
|
||||
CursorValue: 0,
|
||||
SortOrder: SORT_ORDER_NEWEST,
|
||||
PageSize: 50,
|
||||
|
||||
ByUserHandle: h,
|
||||
FilterMedia: REQUIRE,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a cursor for a User's Likes
|
||||
func NewUserFeedLikesCursor(h scraper.UserHandle) Cursor {
|
||||
return Cursor{
|
||||
@ -263,6 +280,7 @@ func (c *Cursor) apply_token(token string) error {
|
||||
c.ToUserHandles = append(c.ToUserHandles, scraper.UserHandle(parts[1]))
|
||||
case "retweeted_by":
|
||||
c.RetweetedByUserHandle = scraper.UserHandle(parts[1])
|
||||
c.FilterRetweets = NONE // Clear the "exclude retweets" filter set by default in NewCursor
|
||||
case "liked_by":
|
||||
c.LikedByUserHandle = scraper.UserHandle(parts[1])
|
||||
case "since":
|
||||
@ -283,6 +301,10 @@ func (c *Cursor) apply_token(token string) error {
|
||||
c.FilterPolls = REQUIRE
|
||||
case "spaces":
|
||||
c.FilterSpaces = REQUIRE
|
||||
case "replies":
|
||||
c.FilterReplies = REQUIRE
|
||||
case "retweets":
|
||||
c.FilterRetweets = REQUIRE
|
||||
}
|
||||
case "-filter":
|
||||
switch parts[1] {
|
||||
@ -298,6 +320,10 @@ func (c *Cursor) apply_token(token string) error {
|
||||
c.FilterPolls = EXCLUDE
|
||||
case "spaces":
|
||||
c.FilterSpaces = EXCLUDE
|
||||
case "replies":
|
||||
c.FilterReplies = EXCLUDE
|
||||
case "retweets":
|
||||
c.FilterRetweets = EXCLUDE
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user