discordo/internal/login/form.go
2025-11-06 21:10:22 -05:00

143 lines
2.9 KiB
Go

package login
import (
"errors"
"log/slog"
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/http"
"github.com/ayn2op/discordo/internal/keyring"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/tview"
"github.com/diamondburned/arikawa/v3/api"
"golang.design/x/clipboard"
)
const (
formPageName = "form"
errorPageName = "error"
qrPageName = "qr"
)
type DoneFn = func(token string)
type Form struct {
*tview.Pages
app *tview.Application
cfg *config.Config
form *tview.Form
done DoneFn
}
func NewForm(app *tview.Application, cfg *config.Config, done DoneFn) *Form {
f := &Form{
Pages: tview.NewPages(),
app: app,
cfg: cfg,
form: tview.NewForm(),
done: done,
}
f.form.
AddInputField("Email", "", 0, nil, nil).
AddPasswordField("Password", "", 0, 0, nil).
AddPasswordField("Code (optional)", "", 0, 0, nil).
AddButton("Login", f.login).
AddButton("Login with QR", f.loginWithQR)
f.AddAndSwitchToPage(formPageName, f.form, true)
return f
}
func (f *Form) login() {
email := f.form.GetFormItem(0).(*tview.InputField).GetText()
password := f.form.GetFormItem(1).(*tview.InputField).GetText()
if email == "" || password == "" {
return
}
// Create an API client without an authentication token.
client := api.NewClient("")
props := http.IdentifyProperties()
if browserUserAgent, ok := props["browser_user_agent"]; ok {
if val, ok := browserUserAgent.(string); ok {
api.UserAgent = val
}
}
resp, err := client.Login(email, password)
if err != nil {
f.onError(err)
return
}
if resp.Token == "" && resp.MFA {
code := f.form.GetFormItem(2).(*tview.InputField).GetText()
if code == "" {
f.onError(errors.New("code required"))
return
}
// Attempt to login using the code.
resp, err = client.TOTP(code, resp.Ticket)
if err != nil {
f.onError(err)
return
}
}
if resp.Token == "" {
f.onError(errors.New("missing token"))
return
}
go keyring.SetToken(resp.Token)
if f.done != nil {
f.done(resp.Token)
}
}
func (f *Form) onError(err error) {
slog.Error("failed to login", "err", err)
message := err.Error()
modal := tview.NewModal().
SetText(message).
AddButtons([]string{"Copy", "Close"}).
SetDoneFunc(func(buttonIndex int, _ string) {
if buttonIndex == 0 {
go clipboard.Write(clipboard.FmtText, []byte(message))
} else {
f.
RemovePage(errorPageName).
SwitchToPage(formPageName)
}
})
f.
AddAndSwitchToPage(errorPageName, ui.Centered(modal, 0, 0), true).
ShowPage(formPageName)
}
func (f *Form) loginWithQR() {
qr := newQRLogin(f.app, f.cfg, func(token string, err error) {
if err != nil {
f.onError(err)
return
}
if token == "" {
f.RemovePage(qrPageName).SwitchToPage(formPageName)
return
}
go keyring.SetToken(token)
f.RemovePage(qrPageName)
if f.done != nil {
f.done(token)
}
})
f.AddAndSwitchToPage(qrPageName, qr, true)
qr.start()
}