Add session management routes and pages ("/login", "/change-session")
- Also enable tweet fetching in TweetDetail handler if tweet is missing or not scraped - Improve some UI stuff w/ more styles - Enable HTMX page swapping in some places instead of full page loads
This commit is contained in:
parent
4a40f35ff6
commit
02bc365add
2
go.mod
2
go.mod
@ -4,12 +4,14 @@ go 1.16
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.2
|
||||
github.com/go-playground/form/v4 v4.2.1
|
||||
github.com/go-test/deep v1.0.7
|
||||
github.com/jarcoal/httpmock v1.1.0
|
||||
github.com/jmoiron/sqlx v1.3.4
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/net v0.9.0
|
||||
golang.org/x/term v0.7.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
4
go.sum
4
go.sum
@ -3,6 +3,10 @@ github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw=
|
||||
github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
|
||||
|
@ -2,12 +2,12 @@ package webserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
|
||||
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
||||
)
|
||||
@ -55,18 +55,40 @@ func (app *Application) buffered_render(w http.ResponseWriter, tpl *template.Tem
|
||||
type TweetCollection interface {
|
||||
Tweet(id scraper.TweetID) scraper.Tweet
|
||||
User(id scraper.UserID) scraper.User
|
||||
FocusedTweetID() scraper.TweetID
|
||||
}
|
||||
|
||||
// Creates a template from the given template file using all the available partials.
|
||||
// Calls `app.buffered_render` to render the created template.
|
||||
func (app *Application) buffered_render_template_for(w http.ResponseWriter, tpl_file string, data TweetCollection) {
|
||||
func (app *Application) buffered_render_tweet_page(w http.ResponseWriter, tpl_file string, data TweetCollection) {
|
||||
partials, err := filepath.Glob(get_filepath("tpl/includes/*.tpl"))
|
||||
panic_if(err)
|
||||
tweet_partials, err := filepath.Glob(get_filepath("tpl/tweet_page_includes/*.tpl"))
|
||||
panic_if(err)
|
||||
partials = append(partials, tweet_partials...)
|
||||
|
||||
tpl, err := template.New("does this matter at all? lol").Funcs(
|
||||
template.FuncMap{"tweet": data.Tweet, "user": data.User},
|
||||
template.FuncMap{"tweet": data.Tweet, "user": data.User, "active_user": app.get_active_user, "focused_tweet_id": data.FocusedTweetID},
|
||||
).ParseFiles(append(partials, get_filepath(tpl_file))...)
|
||||
panic_if(err)
|
||||
|
||||
app.buffered_render(w, tpl, data)
|
||||
}
|
||||
|
||||
// Creates a template from the given template file using all the available partials.
|
||||
// Calls `app.buffered_render` to render the created template.
|
||||
func (app *Application) buffered_render_basic_page(w http.ResponseWriter, tpl_file string, data interface{}) {
|
||||
partials, err := filepath.Glob(get_filepath("tpl/includes/*.tpl"))
|
||||
panic_if(err)
|
||||
|
||||
tpl, err := template.New("does this matter at all? lol").Funcs(
|
||||
template.FuncMap{"active_user": app.get_active_user},
|
||||
).ParseFiles(append(partials, get_filepath(tpl_file))...)
|
||||
panic_if(err)
|
||||
|
||||
app.buffered_render(w, tpl, data)
|
||||
}
|
||||
|
||||
func (app *Application) get_active_user() scraper.User {
|
||||
return app.ActiveUser
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package webserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@ -15,6 +16,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/form/v4"
|
||||
|
||||
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/persistence"
|
||||
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
||||
)
|
||||
@ -30,6 +33,8 @@ type Application struct {
|
||||
Middlewares []Middleware
|
||||
|
||||
Profile persistence.Profile
|
||||
ActiveUser scraper.User
|
||||
DisableScraping bool
|
||||
}
|
||||
|
||||
func NewApp(profile persistence.Profile) Application {
|
||||
@ -40,7 +45,7 @@ func NewApp(profile persistence.Profile) Application {
|
||||
ErrorLog: log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile),
|
||||
|
||||
Profile: profile,
|
||||
// formDecoder: form.NewDecoder(),
|
||||
ActiveUser: get_default_user(),
|
||||
}
|
||||
ret.Middlewares = []Middleware{
|
||||
secureHeaders,
|
||||
@ -85,6 +90,10 @@ func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
app.TweetDetail(w, r)
|
||||
case "content":
|
||||
http.StripPrefix("/content", http.FileServer(http.Dir(app.Profile.ProfileDir))).ServeHTTP(w, r)
|
||||
case "login":
|
||||
app.Login(w, r)
|
||||
case "change-session":
|
||||
app.ChangeSession(w, r)
|
||||
default:
|
||||
app.UserFeed(w, r)
|
||||
}
|
||||
@ -110,13 +119,7 @@ func (app *Application) Run(address string) {
|
||||
|
||||
func (app *Application) Home(w http.ResponseWriter, r *http.Request) {
|
||||
app.traceLog.Printf("'Home' handler (path: %q)", r.URL.Path)
|
||||
tpl, err := template.ParseFiles(
|
||||
get_filepath("tpl/includes/base.tpl"),
|
||||
get_filepath("tpl/includes/nav_sidebar.tpl"),
|
||||
get_filepath("tpl/home.tpl"),
|
||||
)
|
||||
panic_if(err)
|
||||
app.buffered_render(w, tpl, nil)
|
||||
app.buffered_render_basic_page(w, "tpl/home.tpl", nil)
|
||||
}
|
||||
|
||||
type TweetDetailData struct {
|
||||
@ -135,6 +138,9 @@ func (t TweetDetailData) Tweet(id scraper.TweetID) scraper.Tweet {
|
||||
func (t TweetDetailData) User(id scraper.UserID) scraper.User {
|
||||
return t.Users[id]
|
||||
}
|
||||
func (t TweetDetailData) FocusedTweetID() scraper.TweetID {
|
||||
return t.MainTweetID
|
||||
}
|
||||
|
||||
func to_json(t interface{}) string {
|
||||
js, err := json.Marshal(t)
|
||||
@ -147,14 +153,43 @@ func to_json(t interface{}) string {
|
||||
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)
|
||||
tweet_id, err := strconv.Atoi(tail)
|
||||
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 = scraper.TweetID(tweet_id)
|
||||
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 {
|
||||
@ -168,7 +203,7 @@ func (app *Application) TweetDetail(w http.ResponseWriter, r *http.Request) {
|
||||
app.InfoLog.Printf(to_json(trove))
|
||||
data.TweetDetailView = trove
|
||||
|
||||
app.buffered_render_template_for(w, "tpl/tweet_detail.tpl", data)
|
||||
app.buffered_render_tweet_page(w, "tpl/tweet_detail.tpl", data)
|
||||
}
|
||||
|
||||
type UserProfileData struct {
|
||||
@ -182,6 +217,9 @@ func (t UserProfileData) Tweet(id scraper.TweetID) scraper.Tweet {
|
||||
func (t UserProfileData) User(id scraper.UserID) scraper.User {
|
||||
return t.Users[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)
|
||||
@ -195,11 +233,123 @@ func (app *Application) UserFeed(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
feed, err := app.Profile.GetUserFeed(user.ID, 50, scraper.TimestampFromUnix(0))
|
||||
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}
|
||||
app.InfoLog.Printf(to_json(data))
|
||||
|
||||
app.buffered_render_template_for(w, "tpl/user_feed.tpl", data)
|
||||
app.buffered_render_tweet_page(w, "tpl/user_feed.tpl", data)
|
||||
}
|
||||
|
||||
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(err.Error())
|
||||
app.error_400_with_message(w, "Invalid form data")
|
||||
return
|
||||
}
|
||||
form.Validate()
|
||||
if len(form.FormErrors) == 0 {
|
||||
api := scraper.NewGuestSession()
|
||||
api.LogIn(form.Username, form.Password)
|
||||
scraper.InitApi(api)
|
||||
app.Profile.SaveSession(api)
|
||||
http.Redirect(w, r, "/login", 303)
|
||||
}
|
||||
}
|
||||
data := LoginData{
|
||||
LoginForm: form,
|
||||
ExistingSessions: app.Profile.ListSessions(),
|
||||
}
|
||||
app.buffered_render_basic_page(w, "tpl/login.tpl", &data)
|
||||
}
|
||||
|
||||
func get_default_user() scraper.User {
|
||||
return scraper.User{ID: 0, Handle: "[nobody]", DisplayName: "[Not logged in]", ProfileImageLocalPath: path.Base(scraper.DEFAULT_PROFILE_IMAGE_URL), IsContentDownloaded: true}
|
||||
}
|
||||
|
||||
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.error_400_with_message(w, "Invalid form data")
|
||||
return
|
||||
}
|
||||
if form.AccountName == "no account" {
|
||||
// Special value that indicates to use a guest session
|
||||
scraper.InitApi(scraper.NewGuestSession())
|
||||
app.ActiveUser = get_default_user()
|
||||
app.DisableScraping = true // API requests will fail b/c not logged in
|
||||
} else {
|
||||
// Activate the selected session
|
||||
user, err := app.Profile.GetUserByHandle(scraper.UserHandle(form.AccountName))
|
||||
if err != nil {
|
||||
app.error_400_with_message(w, fmt.Sprintf("User not in database: %s", form.AccountName))
|
||||
return
|
||||
}
|
||||
scraper.InitApi(app.Profile.LoadSession(scraper.UserHandle(form.AccountName)))
|
||||
app.ActiveUser = user
|
||||
app.DisableScraping = false
|
||||
}
|
||||
|
||||
tpl, err := template.New("").Funcs(
|
||||
template.FuncMap{"active_user": app.get_active_user},
|
||||
).ParseFiles(
|
||||
get_filepath("tpl/includes/nav_sidebar.tpl"),
|
||||
get_filepath("tpl/includes/author_info.tpl"),
|
||||
)
|
||||
buf := new(bytes.Buffer)
|
||||
err = tpl.ExecuteTemplate(buf, "nav-sidebar", nil)
|
||||
panic_if(err)
|
||||
|
||||
_, err = buf.WriteTo(w)
|
||||
panic_if(err)
|
||||
}
|
||||
|
||||
var formDecoder = form.NewDecoder()
|
||||
|
||||
func parse_form(req *http.Request, result interface{}) error {
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return formDecoder.Decode(result, req.PostForm)
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ func selector(s string) cascadia.Sel {
|
||||
func do_request(req *http.Request) *http.Response {
|
||||
recorder := httptest.NewRecorder()
|
||||
app := webserver.NewApp(profile)
|
||||
app.DisableScraping = true
|
||||
app.ServeHTTP(recorder, req)
|
||||
return recorder.Result()
|
||||
}
|
||||
|
1
internal/webserver/static/icons/dotdotdot.svg
Normal file
1
internal/webserver/static/icons/dotdotdot.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" class="r-1nao33i r-4qtqp9 r-yyyyoo r-1xvli5t r-dnmrzs r-bnwqim r-1plcrui r-lrvibr"><g><path d="M3 12c0-1.1.9-2 2-2s2 .9 2 2-.9 2-2 2-2-.9-2-2zm9 2c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm7 0c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"></path></g></svg>
|
After Width: | Height: | Size: 337 B |
@ -40,10 +40,20 @@ body {
|
||||
main {
|
||||
padding-top: 4em;
|
||||
}
|
||||
input, select {
|
||||
font-family: inherit;
|
||||
font-size: 1em;
|
||||
padding: 0.2em 0.6em;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.tweet {
|
||||
padding: 0 1.5em;
|
||||
}
|
||||
:not(.focused-tweet) > .tweet {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quoted-tweet {
|
||||
padding: 1.3em;
|
||||
@ -69,6 +79,10 @@ main {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.author-info, .tweet-text {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.name-and-handle {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
@ -113,6 +127,7 @@ a.mention {
|
||||
.tweet-text {
|
||||
display: block;
|
||||
margin-bottom: 0.4em;
|
||||
margin-top: 0;
|
||||
/* padding-bottom: 0.5em;*/
|
||||
}
|
||||
.focused-tweet .tweet-text {
|
||||
@ -227,18 +242,19 @@ ul.quick-links {
|
||||
align-items: flex-start;
|
||||
padding: 0 2em;
|
||||
}
|
||||
li.quick-link {
|
||||
.quick-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.5em;
|
||||
margin: 0.2em;
|
||||
border-radius: 100em; /* any large amount, just don't use % because then it makes an ellipse */
|
||||
cursor: pointer;
|
||||
}
|
||||
li.quick-link:hover {
|
||||
.quick-link:hover {
|
||||
background-color: var(--color-twitter-blue-light);
|
||||
}
|
||||
li.quick-link:active {
|
||||
.quick-link:active {
|
||||
transform: translate(2px, 2px);
|
||||
background-color: var(--color-twitter-blue);
|
||||
}
|
||||
@ -310,7 +326,55 @@ svg {
|
||||
}
|
||||
.search-bar {
|
||||
flex-grow: 1;
|
||||
font-size: 1em;
|
||||
font-family: "Titillium Web";
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
|
||||
.login {
|
||||
width: 60%;
|
||||
margin: 20% auto;
|
||||
}
|
||||
.login-form input {
|
||||
width: 100%;
|
||||
border-radius: 0.5em;
|
||||
|
||||
padding: 0.5em 0.6em;
|
||||
}
|
||||
.login-form .error {
|
||||
color: #C0392B;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.login-form .error + input {
|
||||
border-color: #C0392B;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.field-container {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
.submit-container {
|
||||
text-align: right;
|
||||
}
|
||||
input[type="submit"] {
|
||||
background-color: var(--color-twitter-blue-light);
|
||||
width: 10em;
|
||||
padding: 1em;
|
||||
margin-top: 1em;
|
||||
border-radius: 1em;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.change-session-form select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
#logged-in-user-info {
|
||||
font-size: 0.8em;
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quick-link .author-info {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
{{define "author-info"}}
|
||||
<div class="author-info">
|
||||
<div class="author-info" hx-boost="true">
|
||||
<a class="unstyled-link" href="/{{.Handle}}">
|
||||
<img class="profile-image" src="{{if .IsContentDownloaded}}/content/profile_images/{{.ProfileImageLocalPath}}{{else}}{{.ProfileImageUrl}}{{end}}" />
|
||||
<img
|
||||
class="profile-image"
|
||||
src="{{if .IsContentDownloaded}}/content/profile_images/{{.ProfileImageLocalPath}}{{else}}{{.ProfileImageUrl}}{{end}}"
|
||||
/>
|
||||
</a>
|
||||
<span class="name-and-handle">
|
||||
<div class="display-name">{{.DisplayName}}</div>
|
||||
|
@ -7,6 +7,7 @@
|
||||
<link rel='stylesheet' href='/static/styles.css'>
|
||||
<link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
|
||||
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Titillium+Web:400,700'>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.4" integrity="sha384-zUfuhFKKZCbHTY6aRR46gxiqszMk5tcHjsVFxnUo8VMus4kHGVdIYVbOYYNlKmHV" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top-bar">
|
||||
|
@ -1,5 +1,11 @@
|
||||
{{define "nav-sidebar"}}
|
||||
<div class="nav-sidebar">
|
||||
<div id="logged-in-user-info">
|
||||
<div class="quick-link" hx-get="/login" hx-trigger="click" hx-target="body" hx-push-url="true">
|
||||
{{template "author-info" active_user}}
|
||||
<img class="svg-icon" src="/static/icons/dotdotdot.svg" />
|
||||
</div>
|
||||
</div>
|
||||
<ul class="quick-links">
|
||||
<a class="unstyled-link" href="#">
|
||||
<li class="quick-link">
|
||||
@ -49,7 +55,7 @@
|
||||
<span>Verified</span>
|
||||
</li>
|
||||
</a>
|
||||
<a class="unstyled-link" href="#">
|
||||
<a class="unstyled-link" href="/{{(active_user).Handle}}">
|
||||
<li class="quick-link">
|
||||
<img class="svg-icon" src="/static/icons/profile.svg" />
|
||||
<span>Profile</span>
|
||||
|
40
internal/webserver/tpl/login.tpl
Normal file
40
internal/webserver/tpl/login.tpl
Normal file
@ -0,0 +1,40 @@
|
||||
{{define "title"}}Login{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<div class="login">
|
||||
<form hx-post="/change-session" hx-target=".nav-sidebar" hx-swap="outerHTML">
|
||||
<label for="select-account">Choose account:</label>
|
||||
<select name="account" id="select-account">
|
||||
{{range .ExistingSessions}}
|
||||
<option value="{{.}}">@{{.}}</option>
|
||||
{{end}}
|
||||
<option value="no account">[no account (don't log in)]</option>
|
||||
</select>
|
||||
<div class="field-container submit-container">
|
||||
<input type='submit' value='Use account'>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p>Or log in</p>
|
||||
|
||||
<form class="login-form" hx-post="/login" hx-target="body">
|
||||
<div class="field-container">
|
||||
<label>Username</label>
|
||||
{{with .FormErrors.username}}
|
||||
<label class='error'>({{.}})</label>
|
||||
{{end}}
|
||||
<input name='username' value='{{.Username}}'>
|
||||
</div>
|
||||
<div class="field-container">
|
||||
<label>Password:</label>
|
||||
{{with .FormErrors.password}}
|
||||
<label class='error'>({{.}})</label>
|
||||
{{end}}
|
||||
<input type='password' name='password'>
|
||||
</div>
|
||||
<div class="field-container submit-container">
|
||||
<input type='submit' value='Login'>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
@ -1,10 +1,19 @@
|
||||
{{define "tweet"}}
|
||||
<div class="tweet">
|
||||
{{$main_tweet := (tweet .)}}
|
||||
{{$author := (user $main_tweet.UserID)}}
|
||||
|
||||
<div class="tweet"
|
||||
{{if (not (eq $main_tweet.ID (focused_tweet_id)))}}
|
||||
hx-post="/tweet/{{$main_tweet.ID}}"
|
||||
hx-trigger="click target::not(.tweet-text)"
|
||||
hx-target="body"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
{{end}}
|
||||
>
|
||||
<div class="tweet-header-container">
|
||||
<div class="author-info-container" hx-trigger="click consume">
|
||||
{{template "author-info" $author}}
|
||||
</div>
|
||||
{{if $main_tweet.ReplyMentions}}
|
||||
<div class="reply-mentions-container">
|
||||
<span class="replying-to-label">Replying to</span>
|
||||
@ -30,9 +39,9 @@
|
||||
</span>
|
||||
<span class="vertical-container-1">
|
||||
<div class="tweet-content">
|
||||
<a href="/tweet/{{$main_tweet.ID}}" class="unstyled-link tweet-text">
|
||||
<p class="tweet-text">
|
||||
{{$main_tweet.Text}}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{{range $main_tweet.Images}}
|
||||
<img src="/content/images/{{.LocalFilename}}" style="max-width: 45%"/>
|
@ -5,11 +5,14 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
sql "github.com/jmoiron/sqlx"
|
||||
"github.com/jmoiron/sqlx/reflectx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"gitlab.com/offline-twitter/twitter_offline_engine/pkg/scraper"
|
||||
)
|
||||
|
||||
//go:embed schema.sql
|
||||
@ -152,3 +155,15 @@ func LoadProfile(profile_dir string) (Profile, error) {
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (p Profile) ListSessions() []scraper.UserHandle {
|
||||
result, err := filepath.Glob(path.Join(p.ProfileDir, "*.session"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ret := []scraper.UserHandle{}
|
||||
for _, filename := range result {
|
||||
ret = append(ret, scraper.UserHandle(path.Base(filename[:len(filename)-len(".session")])))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user