201 lines
5.1 KiB
Go

package webserver
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
. "gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
)
type ListData struct {
List
Feed persistence.Feed
UserIDs []UserID
ActiveTab string
}
func NewListData(users []User) (ListData, TweetTrove) {
trove := NewTweetTrove()
data := ListData{
UserIDs: []UserID{},
}
for _, u := range users {
trove.Users[u.ID] = u
data.UserIDs = append(data.UserIDs, u.ID)
}
return data, trove
}
func (app *Application) ListDetailFeed(w http.ResponseWriter, r *http.Request) {
list := get_list_from_context(r.Context())
c := persistence.NewListCursor(list.ID)
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, app.ActiveUser.ID)
if err != nil && !errors.Is(err, persistence.ErrEndOfFeed) {
panic(err)
}
if r.Header.Get("HX-Request") == "true" && c.CursorPosition == persistence.CURSOR_MIDDLE {
// It's a Show More request
app.buffered_render_htmx(w, "timeline", PageGlobalData{TweetTrove: feed.TweetTrove}, feed)
} else {
app.buffered_render_page(
w,
"tpl/list.tpl",
PageGlobalData{TweetTrove: feed.TweetTrove},
ListData{Feed: feed, List: list, ActiveTab: "feed"},
)
}
}
func (app *Application) ListDetailUsers(w http.ResponseWriter, r *http.Request) {
list := get_list_from_context(r.Context())
users := app.Profile.GetListUsers(list.ID)
data, trove := NewListData(users)
data.List = list
data.ActiveTab = "users"
app.buffered_render_page(w, "tpl/list.tpl", PageGlobalData{TweetTrove: trove}, data)
}
func (app *Application) ListDelete(w http.ResponseWriter, r *http.Request) {
list := get_list_from_context(r.Context())
app.Profile.DeleteList(list.ID)
http.Redirect(w, r, "/lists", 302)
}
func (app *Application) ListDetail(w http.ResponseWriter, r *http.Request) {
app.traceLog.Printf("'ListDetail' handler (path: %q)", r.URL.Path)
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(parts) == 1 && parts[0] == "" {
switch r.Method {
case "DELETE":
app.ListDelete(w, r)
default:
// No further path; just show the feed
app.ListDetailFeed(w, r)
}
return
}
switch parts[0] {
case "users":
app.ListDetailUsers(w, r)
case "add_user":
app.ListAddUser(w, r)
case "remove_user":
app.ListRemoveUser(w, r)
default:
app.error_404(w)
}
}
func (app *Application) ListAddUser(w http.ResponseWriter, r *http.Request) {
handle := r.URL.Query().Get("user_handle")
if handle[0] == '@' {
handle = handle[1:]
}
user, err := app.Profile.GetUserByHandle(UserHandle(handle))
if err != nil {
app.error_400_with_message(w, "Fetch user: "+err.Error())
return
}
list := get_list_from_context(r.Context())
app.Profile.SaveListUser(list.ID, user.ID)
http.Redirect(w, r, fmt.Sprintf("/lists/%d/users", list.ID), 302)
}
func (app *Application) ListRemoveUser(w http.ResponseWriter, r *http.Request) {
handle := r.URL.Query().Get("user_handle")
if handle[0] == '@' {
handle = handle[1:]
}
user, err := app.Profile.GetUserByHandle(UserHandle(handle))
if err != nil {
app.error_400_with_message(w, "Fetch user: "+err.Error())
return
}
list := get_list_from_context(r.Context())
app.Profile.DeleteListUser(list.ID, user.ID)
http.Redirect(w, r, fmt.Sprintf("/lists/%d/users", list.ID), 302)
}
func (app *Application) Lists(w http.ResponseWriter, r *http.Request) {
app.traceLog.Printf("'Lists' handler (path: %q)", r.URL.Path)
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
// List detail
if parts[0] != "" { // If there's an ID param
_list_id, err := strconv.Atoi(parts[0])
if err != nil {
app.error_400_with_message(w, "List ID must be a number")
return
}
list, err := app.Profile.GetListById(ListID(_list_id))
if err != nil {
app.error_404(w)
return
}
req_with_ctx := r.WithContext(add_list_to_context(r.Context(), list))
http.StripPrefix(fmt.Sprintf("/%d", list.ID), http.HandlerFunc(app.ListDetail)).ServeHTTP(w, req_with_ctx)
return
}
// New list
if r.Method == "POST" {
var formdata struct {
Name string `json:"name"`
}
data, err := io.ReadAll(r.Body)
panic_if(err)
err = json.Unmarshal(data, &formdata)
panic_if(err)
new_list := List{Name: formdata.Name}
app.Profile.SaveList(&new_list)
http.Redirect(w, r, fmt.Sprintf("/lists/%d/users", new_list.ID), 302)
return
}
// List index
lists := app.Profile.GetAllLists()
trove := NewTweetTrove()
for _, l := range lists {
for _, u := range l.Users {
trove.Users[u.ID] = u
}
}
app.buffered_render_page(
w,
"tpl/list_of_lists.tpl",
PageGlobalData{TweetTrove: trove},
lists,
)
}
const LIST_KEY = key("list") // type `key` is defined in "handler_tweet_detail"
func add_list_to_context(ctx context.Context, list List) context.Context {
return context.WithValue(ctx, LIST_KEY, list)
}
func get_list_from_context(ctx context.Context) List {
list, is_ok := ctx.Value(LIST_KEY).(List)
if !is_ok {
panic("List not found in context")
}
return list
}