Break the HTTP handlers into separate files for maintainability
This commit is contained in:
parent
930af3455f
commit
addcf0ea52
55
internal/webserver/handler_follow_unfollow.go
Normal file
55
internal/webserver/handler_follow_unfollow.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) UserFollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
app.traceLog.Printf("'UserFollow' handler (path: %q)", r.URL.Path)
|
||||||
|
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", 405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
app.error_400_with_message(w, "Bad URL: "+r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := app.Profile.GetUserByHandle(scraper.UserHandle(parts[1]))
|
||||||
|
if err != nil {
|
||||||
|
app.error_404(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Profile.SetUserFollowed(&user, true)
|
||||||
|
|
||||||
|
app.buffered_render_basic_htmx(w, "following-button", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) UserUnfollow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
app.traceLog.Printf("'UserUnfollow' handler (path: %q)", r.URL.Path)
|
||||||
|
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", 405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
app.error_400_with_message(w, "Bad URL: "+r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := app.Profile.GetUserByHandle(scraper.UserHandle(parts[1]))
|
||||||
|
if err != nil {
|
||||||
|
app.error_404(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Profile.SetUserFollowed(&user, false)
|
||||||
|
app.buffered_render_basic_htmx(w, "following-button", user)
|
||||||
|
}
|
80
internal/webserver/handler_login.go
Normal file
80
internal/webserver/handler_login.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginData struct {
|
||||||
|
LoginForm
|
||||||
|
ExistingSessions []scraper.UserHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginForm struct {
|
||||||
|
Username string `form:"username"`
|
||||||
|
Password string `form:"password"`
|
||||||
|
FormErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *LoginForm) Validate() {
|
||||||
|
if f.FormErrors == nil {
|
||||||
|
f.FormErrors = make(FormErrors)
|
||||||
|
}
|
||||||
|
if len(f.Username) == 0 {
|
||||||
|
f.FormErrors["username"] = "cannot be blank"
|
||||||
|
}
|
||||||
|
if len(f.Password) == 0 {
|
||||||
|
f.FormErrors["password"] = "cannot be blank"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
app.traceLog.Printf("'Login' handler (path: %q)", r.URL.Path)
|
||||||
|
var form LoginForm
|
||||||
|
if r.Method == "POST" {
|
||||||
|
err := parse_form(r, &form)
|
||||||
|
if err != nil {
|
||||||
|
app.InfoLog.Print("Form error parse: " + err.Error())
|
||||||
|
app.error_400_with_message(w, err.Error())
|
||||||
|
}
|
||||||
|
form.Validate()
|
||||||
|
if len(form.FormErrors) == 0 {
|
||||||
|
api := scraper.NewGuestSession()
|
||||||
|
api.LogIn(form.Username, form.Password)
|
||||||
|
app.Profile.SaveSession(api)
|
||||||
|
if err := app.SetActiveUser(api.UserHandle); err != nil {
|
||||||
|
app.ErrorLog.Printf(err.Error())
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/login", 303)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// method = "GET"
|
||||||
|
data := LoginData{
|
||||||
|
LoginForm: form,
|
||||||
|
ExistingSessions: app.Profile.ListSessions(),
|
||||||
|
}
|
||||||
|
app.buffered_render_basic_page(w, "tpl/login.tpl", &data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) ChangeSession(w http.ResponseWriter, r *http.Request) {
|
||||||
|
app.traceLog.Printf("'change-session' handler (path: %q)", r.URL.Path)
|
||||||
|
form := struct {
|
||||||
|
AccountName string `form:"account"`
|
||||||
|
}{}
|
||||||
|
err := parse_form(r, &form)
|
||||||
|
if err != nil {
|
||||||
|
app.InfoLog.Print("Form error parse: " + err.Error())
|
||||||
|
app.error_400_with_message(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = app.SetActiveUser(scraper.UserHandle(form.AccountName))
|
||||||
|
if err != nil {
|
||||||
|
app.error_400_with_message(w, fmt.Sprintf("User not in database: %s", form.AccountName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.buffered_render_basic_htmx(w, "nav-sidebar", nil)
|
||||||
|
}
|
37
internal/webserver/handler_timeline.go
Normal file
37
internal/webserver/handler_timeline.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *Application) Timeline(w http.ResponseWriter, r *http.Request) {
|
||||||
|
app.traceLog.Printf("'Timeline' handler (path: %q)", r.URL.Path)
|
||||||
|
|
||||||
|
c := persistence.NewTimelineCursor()
|
||||||
|
err := parse_cursor_value(&c, r)
|
||||||
|
if err != nil {
|
||||||
|
app.error_400_with_message(w, "invalid cursor (must be a number)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := app.Profile.NextPage(c)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, persistence.ErrEndOfFeed) {
|
||||||
|
// TODO
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := UserProfileData{Feed: feed} // TODO: wrong struct
|
||||||
|
|
||||||
|
if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE {
|
||||||
|
// It's a Show More request
|
||||||
|
app.buffered_render_tweet_htmx(w, "timeline", data)
|
||||||
|
} else {
|
||||||
|
app.buffered_render_tweet_page(w, "tpl/offline_timeline.tpl", data)
|
||||||
|
}
|
||||||
|
}
|
93
internal/webserver/handler_tweet_detail.go
Normal file
93
internal/webserver/handler_tweet_detail.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
|
||||||
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TweetDetailData struct {
|
||||||
|
persistence.TweetDetailView
|
||||||
|
MainTweetID scraper.TweetID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTweetDetailData() TweetDetailData {
|
||||||
|
return TweetDetailData{
|
||||||
|
TweetDetailView: persistence.NewTweetDetailView(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (t TweetDetailData) Tweet(id scraper.TweetID) scraper.Tweet {
|
||||||
|
return t.Tweets[id]
|
||||||
|
}
|
||||||
|
func (t TweetDetailData) User(id scraper.UserID) scraper.User {
|
||||||
|
return t.Users[id]
|
||||||
|
}
|
||||||
|
func (t TweetDetailData) Retweet(id scraper.TweetID) scraper.Retweet {
|
||||||
|
return t.Retweets[id]
|
||||||
|
}
|
||||||
|
func (t TweetDetailData) Space(id scraper.SpaceID) scraper.Space {
|
||||||
|
return t.Spaces[id]
|
||||||
|
}
|
||||||
|
func (t TweetDetailData) FocusedTweetID() scraper.TweetID {
|
||||||
|
return t.MainTweetID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) {
|
||||||
|
app.traceLog.Printf("'TweetDetail' handler (path: %q)", r.URL.Path)
|
||||||
|
_, tail := path.Split(r.URL.Path)
|
||||||
|
val, err := strconv.Atoi(tail)
|
||||||
|
if err != nil {
|
||||||
|
app.error_400_with_message(w, fmt.Sprintf("Invalid tweet ID: %q", tail))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tweet_id := scraper.TweetID(val)
|
||||||
|
|
||||||
|
data := NewTweetDetailData()
|
||||||
|
data.MainTweetID = tweet_id
|
||||||
|
|
||||||
|
// Return whether the scrape succeeded (if false, we should 404)
|
||||||
|
try_scrape_tweet := func() bool {
|
||||||
|
if app.DisableScraping {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
trove, err := scraper.GetTweetFullAPIV2(tweet_id, 50) // TODO: parameterizable
|
||||||
|
if err != nil {
|
||||||
|
app.ErrorLog.Print(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
app.Profile.SaveTweetTrove(trove)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tweet, err := app.Profile.GetTweetById(tweet_id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, persistence.ErrNotInDB) {
|
||||||
|
if !try_scrape_tweet() {
|
||||||
|
app.error_404(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
} else if !tweet.IsConversationScraped {
|
||||||
|
try_scrape_tweet() // If it fails, we can still render it (not 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
trove, err := app.Profile.GetTweetDetail(data.MainTweetID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, persistence.ErrNotInDB) {
|
||||||
|
app.error_404(w)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.TweetDetailView = trove
|
||||||
|
|
||||||
|
app.buffered_render_tweet_page(w, "tpl/tweet_detail.tpl", data)
|
||||||
|
}
|
72
internal/webserver/handler_user_feed.go
Normal file
72
internal/webserver/handler_user_feed.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
|
||||||
|
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserProfileData struct {
|
||||||
|
persistence.Feed
|
||||||
|
scraper.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t UserProfileData) Tweet(id scraper.TweetID) scraper.Tweet {
|
||||||
|
return t.Tweets[id]
|
||||||
|
}
|
||||||
|
func (t UserProfileData) User(id scraper.UserID) scraper.User {
|
||||||
|
return t.Users[id]
|
||||||
|
}
|
||||||
|
func (t UserProfileData) Retweet(id scraper.TweetID) scraper.Retweet {
|
||||||
|
return t.Retweets[id]
|
||||||
|
}
|
||||||
|
func (t UserProfileData) Space(id scraper.SpaceID) scraper.Space {
|
||||||
|
return t.Spaces[id]
|
||||||
|
}
|
||||||
|
func (t UserProfileData) FocusedTweetID() scraper.TweetID {
|
||||||
|
return scraper.TweetID(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
app.traceLog.Printf("'UserFeed' handler (path: %q)", r.URL.Path)
|
||||||
|
|
||||||
|
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||||
|
if len(parts) != 1 {
|
||||||
|
app.error_404(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := app.Profile.GetUserByHandle(scraper.UserHandle(parts[0]))
|
||||||
|
if err != nil {
|
||||||
|
app.error_404(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c := persistence.NewUserFeedCursor(user.Handle)
|
||||||
|
err = parse_cursor_value(&c, r)
|
||||||
|
if err != nil {
|
||||||
|
app.error_400_with_message(w, "invalid cursor (must be a number)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := app.Profile.NextPage(c)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, persistence.ErrEndOfFeed) {
|
||||||
|
// TODO
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feed.Users[user.ID] = user
|
||||||
|
|
||||||
|
data := UserProfileData{Feed: feed, UserID: user.ID}
|
||||||
|
|
||||||
|
if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE {
|
||||||
|
// It's a Show More request
|
||||||
|
app.buffered_render_tweet_htmx(w, "timeline", data)
|
||||||
|
} else {
|
||||||
|
app.buffered_render_tweet_page(w, "tpl/user_feed.tpl", data)
|
||||||
|
}
|
||||||
|
}
|
@ -91,7 +91,7 @@ func get_default_user() scraper.User {
|
|||||||
// I don't like the weird matching behavior of http.ServeMux, and it's not hard to write by hand.
|
// I don't like the weird matching behavior of http.ServeMux, and it's not hard to write by hand.
|
||||||
func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
app.Home(w, r)
|
http.Redirect(w, r, "/timeline", 303)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,37 +141,6 @@ func (app *Application) Run(address string) {
|
|||||||
app.ErrorLog.Fatal(err)
|
app.ErrorLog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) Home(w http.ResponseWriter, r *http.Request) {
|
|
||||||
app.traceLog.Printf("'Home' handler (path: %q)", r.URL.Path)
|
|
||||||
app.buffered_render_basic_page(w, "tpl/home.tpl", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TweetDetailData struct {
|
|
||||||
persistence.TweetDetailView
|
|
||||||
MainTweetID scraper.TweetID
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTweetDetailData() TweetDetailData {
|
|
||||||
return TweetDetailData{
|
|
||||||
TweetDetailView: persistence.NewTweetDetailView(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (t TweetDetailData) Tweet(id scraper.TweetID) scraper.Tweet {
|
|
||||||
return t.Tweets[id]
|
|
||||||
}
|
|
||||||
func (t TweetDetailData) User(id scraper.UserID) scraper.User {
|
|
||||||
return t.Users[id]
|
|
||||||
}
|
|
||||||
func (t TweetDetailData) Retweet(id scraper.TweetID) scraper.Retweet {
|
|
||||||
return t.Retweets[id]
|
|
||||||
}
|
|
||||||
func (t TweetDetailData) Space(id scraper.SpaceID) scraper.Space {
|
|
||||||
return t.Spaces[id]
|
|
||||||
}
|
|
||||||
func (t TweetDetailData) FocusedTweetID() scraper.TweetID {
|
|
||||||
return t.MainTweetID
|
|
||||||
}
|
|
||||||
|
|
||||||
// func to_json(t interface{}) string {
|
// func to_json(t interface{}) string {
|
||||||
// js, err := json.Marshal(t)
|
// js, err := json.Marshal(t)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
@ -180,82 +149,6 @@ func (t TweetDetailData) FocusedTweetID() scraper.TweetID {
|
|||||||
// return string(js)
|
// return string(js)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) {
|
|
||||||
app.traceLog.Printf("'TweetDetail' handler (path: %q)", r.URL.Path)
|
|
||||||
_, tail := path.Split(r.URL.Path)
|
|
||||||
val, err := strconv.Atoi(tail)
|
|
||||||
if err != nil {
|
|
||||||
app.error_400_with_message(w, fmt.Sprintf("Invalid tweet ID: %q", tail))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tweet_id := scraper.TweetID(val)
|
|
||||||
|
|
||||||
data := NewTweetDetailData()
|
|
||||||
data.MainTweetID = tweet_id
|
|
||||||
|
|
||||||
// Return whether the scrape succeeded (if false, we should 404)
|
|
||||||
try_scrape_tweet := func() bool {
|
|
||||||
if app.DisableScraping {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
trove, err := scraper.GetTweetFullAPIV2(tweet_id, 50) // TODO: parameterizable
|
|
||||||
if err != nil {
|
|
||||||
app.ErrorLog.Print(err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
app.Profile.SaveTweetTrove(trove)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
tweet, err := app.Profile.GetTweetById(tweet_id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, persistence.ErrNotInDB) {
|
|
||||||
if !try_scrape_tweet() {
|
|
||||||
app.error_404(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
} else if !tweet.IsConversationScraped {
|
|
||||||
try_scrape_tweet() // If it fails, we can still render it (not 404)
|
|
||||||
}
|
|
||||||
|
|
||||||
trove, err := app.Profile.GetTweetDetail(data.MainTweetID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, persistence.ErrNotInDB) {
|
|
||||||
app.error_404(w)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.TweetDetailView = trove
|
|
||||||
|
|
||||||
app.buffered_render_tweet_page(w, "tpl/tweet_detail.tpl", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserProfileData struct {
|
|
||||||
persistence.Feed
|
|
||||||
scraper.UserID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t UserProfileData) Tweet(id scraper.TweetID) scraper.Tweet {
|
|
||||||
return t.Tweets[id]
|
|
||||||
}
|
|
||||||
func (t UserProfileData) User(id scraper.UserID) scraper.User {
|
|
||||||
return t.Users[id]
|
|
||||||
}
|
|
||||||
func (t UserProfileData) Retweet(id scraper.TweetID) scraper.Retweet {
|
|
||||||
return t.Retweets[id]
|
|
||||||
}
|
|
||||||
func (t UserProfileData) Space(id scraper.SpaceID) scraper.Space {
|
|
||||||
return t.Spaces[id]
|
|
||||||
}
|
|
||||||
func (t UserProfileData) FocusedTweetID() scraper.TweetID {
|
|
||||||
return scraper.TweetID(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parse_cursor_value(c *persistence.Cursor, r *http.Request) error {
|
func parse_cursor_value(c *persistence.Cursor, r *http.Request) error {
|
||||||
cursor_param := r.URL.Query().Get("cursor")
|
cursor_param := r.URL.Query().Get("cursor")
|
||||||
if cursor_param != "" {
|
if cursor_param != "" {
|
||||||
@ -269,150 +162,8 @@ func parse_cursor_value(c *persistence.Cursor, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
|
|
||||||
app.traceLog.Printf("'UserFeed' handler (path: %q)", r.URL.Path)
|
|
||||||
|
|
||||||
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
|
||||||
if len(parts) != 1 {
|
|
||||||
app.error_404(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := app.Profile.GetUserByHandle(scraper.UserHandle(parts[0]))
|
|
||||||
if err != nil {
|
|
||||||
app.error_404(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c := persistence.NewUserFeedCursor(user.Handle)
|
|
||||||
err = parse_cursor_value(&c, r)
|
|
||||||
if err != nil {
|
|
||||||
app.error_400_with_message(w, "invalid cursor (must be a number)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
feed, err := app.Profile.NextPage(c)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, persistence.ErrEndOfFeed) {
|
|
||||||
// TODO
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
feed.Users[user.ID] = user
|
|
||||||
|
|
||||||
data := UserProfileData{Feed: feed, UserID: user.ID}
|
|
||||||
|
|
||||||
if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE {
|
|
||||||
// It's a Show More request
|
|
||||||
app.buffered_render_tweet_htmx(w, "timeline", data)
|
|
||||||
} else {
|
|
||||||
app.buffered_render_tweet_page(w, "tpl/user_feed.tpl", data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) Timeline(w http.ResponseWriter, r *http.Request) {
|
|
||||||
app.traceLog.Printf("'Timeline' handler (path: %q)", r.URL.Path)
|
|
||||||
|
|
||||||
c := persistence.NewTimelineCursor()
|
|
||||||
err := parse_cursor_value(&c, r)
|
|
||||||
if err != nil {
|
|
||||||
app.error_400_with_message(w, "invalid cursor (must be a number)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
feed, err := app.Profile.NextPage(c)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, persistence.ErrEndOfFeed) {
|
|
||||||
// TODO
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data := UserProfileData{Feed: feed} // TODO: wrong struct
|
|
||||||
|
|
||||||
if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE {
|
|
||||||
// It's a Show More request
|
|
||||||
app.buffered_render_tweet_htmx(w, "timeline", data)
|
|
||||||
} else {
|
|
||||||
app.buffered_render_tweet_page(w, "tpl/offline_timeline.tpl", data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormErrors map[string]string
|
type FormErrors map[string]string
|
||||||
|
|
||||||
type LoginForm struct {
|
|
||||||
Username string `form:"username"`
|
|
||||||
Password string `form:"password"`
|
|
||||||
FormErrors
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *LoginForm) Validate() {
|
|
||||||
if f.FormErrors == nil {
|
|
||||||
f.FormErrors = make(FormErrors)
|
|
||||||
}
|
|
||||||
if len(f.Username) == 0 {
|
|
||||||
f.FormErrors["username"] = "cannot be blank"
|
|
||||||
}
|
|
||||||
if len(f.Password) == 0 {
|
|
||||||
f.FormErrors["password"] = "cannot be blank"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginData struct {
|
|
||||||
LoginForm
|
|
||||||
ExistingSessions []scraper.UserHandle
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) Login(w http.ResponseWriter, r *http.Request) {
|
|
||||||
app.traceLog.Printf("'Login' handler (path: %q)", r.URL.Path)
|
|
||||||
var form LoginForm
|
|
||||||
if r.Method == "POST" {
|
|
||||||
err := parse_form(r, &form)
|
|
||||||
if err != nil {
|
|
||||||
app.InfoLog.Print("Form error parse: " + err.Error())
|
|
||||||
app.error_400_with_message(w, err.Error())
|
|
||||||
}
|
|
||||||
form.Validate()
|
|
||||||
if len(form.FormErrors) == 0 {
|
|
||||||
api := scraper.NewGuestSession()
|
|
||||||
api.LogIn(form.Username, form.Password)
|
|
||||||
app.Profile.SaveSession(api)
|
|
||||||
if err := app.SetActiveUser(api.UserHandle); err != nil {
|
|
||||||
app.ErrorLog.Printf(err.Error())
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, "/login", 303)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// method = "GET"
|
|
||||||
data := LoginData{
|
|
||||||
LoginForm: form,
|
|
||||||
ExistingSessions: app.Profile.ListSessions(),
|
|
||||||
}
|
|
||||||
app.buffered_render_basic_page(w, "tpl/login.tpl", &data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) ChangeSession(w http.ResponseWriter, r *http.Request) {
|
|
||||||
app.traceLog.Printf("'change-session' handler (path: %q)", r.URL.Path)
|
|
||||||
form := struct {
|
|
||||||
AccountName string `form:"account"`
|
|
||||||
}{}
|
|
||||||
err := parse_form(r, &form)
|
|
||||||
if err != nil {
|
|
||||||
app.InfoLog.Print("Form error parse: " + err.Error())
|
|
||||||
app.error_400_with_message(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = app.SetActiveUser(scraper.UserHandle(form.AccountName))
|
|
||||||
if err != nil {
|
|
||||||
app.error_400_with_message(w, fmt.Sprintf("User not in database: %s", form.AccountName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.buffered_render_basic_htmx(w, "nav-sidebar", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var formDecoder = form.NewDecoder()
|
var formDecoder = form.NewDecoder()
|
||||||
var (
|
var (
|
||||||
ErrCorruptedFormData = errors.New("corrupted form data")
|
ErrCorruptedFormData = errors.New("corrupted form data")
|
||||||
@ -430,50 +181,3 @@ func parse_form(req *http.Request, result interface{}) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) UserFollow(w http.ResponseWriter, r *http.Request) {
|
|
||||||
app.traceLog.Printf("'UserFollow' handler (path: %q)", r.URL.Path)
|
|
||||||
|
|
||||||
if r.Method != "POST" {
|
|
||||||
http.Error(w, "Method not allowed", 405)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
app.error_400_with_message(w, "Bad URL: "+r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user, err := app.Profile.GetUserByHandle(scraper.UserHandle(parts[1]))
|
|
||||||
if err != nil {
|
|
||||||
app.error_404(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Profile.SetUserFollowed(&user, true)
|
|
||||||
|
|
||||||
app.buffered_render_basic_htmx(w, "following-button", user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) UserUnfollow(w http.ResponseWriter, r *http.Request) {
|
|
||||||
app.traceLog.Printf("'UserUnfollow' handler (path: %q)", r.URL.Path)
|
|
||||||
|
|
||||||
if r.Method != "POST" {
|
|
||||||
http.Error(w, "Method not allowed", 405)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
app.error_400_with_message(w, "Bad URL: "+r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user, err := app.Profile.GetUserByHandle(scraper.UserHandle(parts[1]))
|
|
||||||
if err != nil {
|
|
||||||
app.error_404(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Profile.SetUserFollowed(&user, false)
|
|
||||||
app.buffered_render_basic_htmx(w, "following-button", user)
|
|
||||||
}
|
|
||||||
|
@ -53,17 +53,13 @@ func do_request(req *http.Request) *http.Response {
|
|||||||
// Homepage
|
// Homepage
|
||||||
// --------
|
// --------
|
||||||
|
|
||||||
|
// Should redirect to the timeline
|
||||||
func TestHomepage(t *testing.T) {
|
func TestHomepage(t *testing.T) {
|
||||||
assert := assert.New(t)
|
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
resp := do_request(httptest.NewRequest("GET", "/", nil))
|
resp := do_request(httptest.NewRequest("GET", "/", nil))
|
||||||
require.Equal(resp.StatusCode, 200)
|
require.Equal(resp.StatusCode, 303)
|
||||||
|
require.Equal(resp.Header.Get("Location"), "/timeline")
|
||||||
root, err := html.Parse(resp.Body)
|
|
||||||
require.NoError(err)
|
|
||||||
title_node := cascadia.Query(root, selector("title"))
|
|
||||||
assert.Equal(title_node.FirstChild.Data, "Offline Twitter | Home")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// User feed
|
// User feed
|
||||||
|
Loading…
x
Reference in New Issue
Block a user