diff --git a/internal/webserver/handler_lists.go b/internal/webserver/handler_lists.go index 5c67351..76ef564 100644 --- a/internal/webserver/handler_lists.go +++ b/internal/webserver/handler_lists.go @@ -18,5 +18,5 @@ func (app *Application) Lists(w http.ResponseWriter, r *http.Request) { where is_followed = 1`) panic_if(err) - app.buffered_render_basic_page(w, "tpl/list.tpl", users) + app.buffered_render_basic_page(w, "tpl/list.tpl", ListData{Title: "Offline Follows", Users: users}) } diff --git a/internal/webserver/handler_tweet_detail.go b/internal/webserver/handler_tweet_detail.go index 21e1c6f..e9bf3dd 100644 --- a/internal/webserver/handler_tweet_detail.go +++ b/internal/webserver/handler_tweet_detail.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" "strings" + "context" "gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence" "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper" @@ -151,3 +152,19 @@ func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) { app.buffered_render_tweet_page(w, "tpl/tweet_detail.tpl", data) } + +type key string + +const TWEET_KEY = key("tweet") + +func add_tweet_to_context(ctx context.Context, tweet scraper.Tweet) context.Context { + return context.WithValue(ctx, TWEET_KEY, tweet) +} + +func get_tweet_from_context(ctx context.Context) scraper.Tweet { + tweet, is_ok := ctx.Value(TWEET_KEY).(scraper.Tweet) + if !is_ok { + panic("Tweet not found in context") + } + return tweet +} diff --git a/internal/webserver/handler_user_feed.go b/internal/webserver/handler_user_feed.go index a4b8c58..492e03d 100644 --- a/internal/webserver/handler_user_feed.go +++ b/internal/webserver/handler_user_feed.go @@ -1,6 +1,7 @@ package webserver import ( + "fmt" "errors" "net/http" "strings" @@ -49,6 +50,15 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) { panic_if(app.Profile.DownloadUserContentFor(&user)) } + if len(parts) > 1 && parts[1] == "followers" { + app.UserFollowers(w, r, user) + return + } + if len(parts) > 1 && parts[1] == "followees" { + app.UserFollowees(w, r, user) + return + } + if r.URL.Query().Has("scrape") { if app.IsScrapingDisabled { app.InfoLog.Printf("Would have scraped: %s", r.URL.Path) @@ -118,3 +128,21 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) { app.buffered_render_tweet_page(w, "tpl/user_feed.tpl", data) } } + +type ListData struct { + Title string + Users []scraper.User +} + +func (app *Application) UserFollowees(w http.ResponseWriter, r *http.Request, user scraper.User) { + app.buffered_render_basic_page(w, "tpl/list.tpl", ListData{ + Title: fmt.Sprintf("Followed by @%s", user.Handle), + Users: app.Profile.GetFollowees(user.ID), + }) +} +func (app *Application) UserFollowers(w http.ResponseWriter, r *http.Request, user scraper.User) { + app.buffered_render_basic_page(w, "tpl/list.tpl", ListData{ + Title: fmt.Sprintf("Followers of @%s", user.Handle), + Users: app.Profile.GetFollowers(user.ID), + }) +} diff --git a/internal/webserver/response_helpers.go b/internal/webserver/response_helpers.go index b0e6adb..17eec8d 100644 --- a/internal/webserver/response_helpers.go +++ b/internal/webserver/response_helpers.go @@ -2,7 +2,6 @@ package webserver import ( "bytes" - "context" "fmt" "html/template" "io" @@ -249,19 +248,3 @@ func cursor_to_query_params(c persistence.Cursor) string { result.Set("sort-order", c.SortOrder.String()) return result.Encode() } - -type key string - -const TWEET_KEY = key("tweet") - -func add_tweet_to_context(ctx context.Context, tweet scraper.Tweet) context.Context { - return context.WithValue(ctx, TWEET_KEY, tweet) -} - -func get_tweet_from_context(ctx context.Context) scraper.Tweet { - tweet, is_ok := ctx.Value(TWEET_KEY).(scraper.Tweet) - if !is_ok { - panic("Tweet not found in context") - } - return tweet -} diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index 6325eee..a57880f 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -79,7 +79,7 @@ func TestUserFeed(t *testing.T) { root, err := html.Parse(resp.Body) require.NoError(err) title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "Offline Twitter | @Cernovich") + assert.Equal(title_node.FirstChild.Data, "@Cernovich | Offline Twitter") tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) assert.Len(tweet_nodes, 7) @@ -119,7 +119,7 @@ func TestUserFeedWithCursor(t *testing.T) { root, err := html.Parse(resp.Body) require.NoError(err) title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "Offline Twitter | @Cernovich") + assert.Equal(title_node.FirstChild.Data, "@Cernovich | Offline Twitter") tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) assert.Len(tweet_nodes, 2) @@ -181,6 +181,34 @@ func TestUserFeedLikesTab(t *testing.T) { assert.Len(tweets, 4) } +// Followers and followees +// ----------------------- + +func TestUserFollowers(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/Offline_Twatter/followers", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".users-list-container > .user")), 2) +} + + +func TestUserFollowees(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + resp := do_request(httptest.NewRequest("GET", "/Offline_Twatter/followees", nil)) + require.Equal(resp.StatusCode, 200) + + root, err := html.Parse(resp.Body) + require.NoError(err) + assert.Len(cascadia.QueryAll(root, selector(".users-list-container > .user")), 1) +} + // Timeline page // ------------- @@ -194,7 +222,7 @@ func TestTimeline(t *testing.T) { root, err := html.Parse(resp.Body) require.NoError(err) title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "Offline Twitter | Timeline") + assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter") tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) assert.Len(tweet_nodes, 18) @@ -210,7 +238,7 @@ func TestTimelineWithCursor(t *testing.T) { root, err := html.Parse(resp.Body) require.NoError(err) title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "Offline Twitter | Timeline") + assert.Equal(title_node.FirstChild.Data, "Timeline | Offline Twitter") tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) assert.Len(tweet_nodes, 10) @@ -245,7 +273,7 @@ func TestSearch(t *testing.T) { root, err := html.Parse(resp.Body) require.NoError(err) title_node := cascadia.Query(root, selector("title")) - assert.Equal(title_node.FirstChild.Data, "Offline Twitter | Search") + assert.Equal(title_node.FirstChild.Data, "Search | Offline Twitter") tweet_nodes := cascadia.QueryAll(root, selector(".timeline > .tweet")) assert.Len(tweet_nodes, 1) diff --git a/internal/webserver/tpl/includes/base.tpl b/internal/webserver/tpl/includes/base.tpl index be7e25f..050d5d9 100644 --- a/internal/webserver/tpl/includes/base.tpl +++ b/internal/webserver/tpl/includes/base.tpl @@ -3,7 +3,7 @@ - Offline Twitter | {{template "title" .}} + {{template "title" .}} | Offline Twitter diff --git a/internal/webserver/tpl/list.tpl b/internal/webserver/tpl/list.tpl index b63f7f9..adca996 100644 --- a/internal/webserver/tpl/list.tpl +++ b/internal/webserver/tpl/list.tpl @@ -1,5 +1,5 @@ -{{define "title"}}Followed Users{{end}} +{{define "title"}}{{.Title}}{{end}} {{define "main"}} - {{template "list" .}} + {{template "list" .Users}} {{end}} diff --git a/internal/webserver/tpl/user_feed.tpl b/internal/webserver/tpl/user_feed.tpl index d9d0b15..07fec73 100644 --- a/internal/webserver/tpl/user_feed.tpl +++ b/internal/webserver/tpl/user_feed.tpl @@ -37,14 +37,14 @@
-
+ {{$user.FollowersCount}} followers -
-
+ + is following {{$user.FollowingCount}} -
+
diff --git a/pkg/persistence/follow_queries.go b/pkg/persistence/follow_queries.go index 0cd41cf..6aacdac 100644 --- a/pkg/persistence/follow_queries.go +++ b/pkg/persistence/follow_queries.go @@ -39,18 +39,26 @@ func (p Profile) IsXFollowingY(follower_id UserID, followee_id UserID) bool { return rows.Next() // true if there is a row, false otherwise } -func (p Profile) GetFollowers(followee_id UserID) []UserID { - var ret []UserID - err := p.DB.Select(&ret, `select follower_id from follows where followee_id = ?`, followee_id) +func (p Profile) GetFollowers(followee_id UserID) []User { + var ret []User + err := p.DB.Select(&ret, ` + select `+USERS_ALL_SQL_FIELDS+` + from users + where id in (select follower_id from follows where followee_id = ?) + `, followee_id) if err != nil { panic(err) } return ret } -func (p Profile) GetFollowees(follower_id UserID) []UserID { - var ret []UserID - err := p.DB.Select(&ret, `select followee_id from follows where follower_id = ?`, follower_id) +func (p Profile) GetFollowees(follower_id UserID) []User { + var ret []User + err := p.DB.Select(&ret, ` + select `+USERS_ALL_SQL_FIELDS+` + from users + where id in (select followee_id from follows where follower_id = ?) + `, follower_id) if err != nil { panic(err) } diff --git a/pkg/persistence/follow_queries_test.go b/pkg/persistence/follow_queries_test.go index dbadea4..6638d32 100644 --- a/pkg/persistence/follow_queries_test.go +++ b/pkg/persistence/follow_queries_test.go @@ -34,11 +34,11 @@ func TestSaveAndLoadFollows(t *testing.T) { // Save and reload it profile.SaveAsFolloweesList(follower.ID, trove) - new_followee_ids := profile.GetFollowees(follower.ID) + new_followees := profile.GetFollowees(follower.ID) - assert.Len(new_followee_ids, len(followee_ids)) - for _, id := range new_followee_ids { - _, is_ok := trove.Users[id] + assert.Len(new_followees, len(followee_ids)) + for _, followee := range new_followees { + _, is_ok := trove.Users[followee.ID] assert.True(is_ok) } } diff --git a/sample_data/seed_data.sql b/sample_data/seed_data.sql index 5707806..a1e39f6 100644 --- a/sample_data/seed_data.sql +++ b/sample_data/seed_data.sql @@ -421,7 +421,8 @@ create index if not exists index_follows_followee_id on follows (followee_id); create index if not exists index_follows_follower_id on follows (follower_id); insert into follows values (1, 1178839081222115328, 1488963321701171204), - (2, 1032468021485293568, 1488963321701171204); + (2, 1032468021485293568, 1488963321701171204), + (3, 1488963321701171204, 1240784920831762433); create table fake_user_sequence(latest_fake_id integer not null);