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 is_htmx(r) && 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 }