mirror of https://github.com/Bios-Marcel/cordless
338 lines
12 KiB
Go
338 lines
12 KiB
Go
package ui
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
|
|
"github.com/Bios-Marcel/cordless/shortcuts"
|
|
"github.com/Bios-Marcel/cordless/tview"
|
|
"github.com/Bios-Marcel/discordgo"
|
|
"github.com/atotto/clipboard"
|
|
tcell "github.com/gdamore/tcell/v2"
|
|
|
|
"github.com/Bios-Marcel/cordless/ui/tviewutil"
|
|
"github.com/Bios-Marcel/cordless/util/text"
|
|
)
|
|
|
|
const splashText = `
|
|
██████╗ ██████╗ ██████╗ ██████╗ ██╗ ███████╗███████╗███████╗
|
|
██╔════╝██╔═══██╗██╔══██╗██╔══██╗██║ ██╔════╝██╔════╝██╔════╝ / \
|
|
██║ ██║ ██║██████╔╝██║ ██║██║ █████╗ ███████╗███████╗ ████- -████
|
|
██║ ██║ ██║██╔══██╗██║ ██║██║ ██╔══╝ ╚════██║╚════██║ \ /
|
|
╚██████╗╚██████╔╝██║ ██║██████╔╝███████╗███████╗███████║███████║
|
|
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚══════╝╚══════╝╚══════╝ `
|
|
|
|
type LoginType int
|
|
|
|
const (
|
|
None LoginType = 0
|
|
Token LoginType = 1
|
|
Password LoginType = 2
|
|
)
|
|
|
|
type Login struct {
|
|
*tview.Flex
|
|
app *tview.Application
|
|
tokenInput *tview.InputField
|
|
tokenInputMasked bool
|
|
tokenInputMaskRune rune
|
|
usernameInput *tview.InputField
|
|
passwordInput *tview.InputField
|
|
tfaTokenInput *tview.InputField
|
|
loginType LoginType
|
|
|
|
loginTypeTokenButton *tview.Button
|
|
loginTypePasswordButton *tview.Button
|
|
sessionChannel chan *loginAttempt
|
|
messageText *tview.TextView
|
|
|
|
content *tview.Flex
|
|
loginChoiceView tview.Primitive
|
|
passwordInputView tview.Primitive
|
|
tokenInputView tview.Primitive
|
|
}
|
|
|
|
type loginAttempt struct {
|
|
session *discordgo.Session
|
|
loginError error
|
|
}
|
|
|
|
// NewLogin creates a new login screen with the login components hidden by default.
|
|
func NewLogin(app *tview.Application, configDir string) *Login {
|
|
login := &Login{
|
|
Flex: tview.NewFlex().SetDirection(tview.FlexRow),
|
|
app: app,
|
|
sessionChannel: make(chan *loginAttempt, 1),
|
|
tokenInput: tview.NewInputField(),
|
|
usernameInput: tview.NewInputField(),
|
|
passwordInput: tview.NewInputField(),
|
|
tfaTokenInput: tview.NewInputField(),
|
|
content: tview.NewFlex().SetDirection(tview.FlexRow),
|
|
tokenInputMasked: true,
|
|
tokenInputMaskRune: '*',
|
|
loginTypeTokenButton: tview.NewButton("Login via Authentication-Token"),
|
|
loginTypePasswordButton: tview.NewButton("Login via E-Mail and password (Optionally Supports 2FA)"),
|
|
messageText: tview.NewTextView(),
|
|
}
|
|
|
|
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if shortcuts.ExitApplication.Equals(event) {
|
|
app.Stop()
|
|
//We call exit as we'd otherwise be waiting for a value
|
|
//from the runNext channel.
|
|
os.Exit(0)
|
|
return nil
|
|
}
|
|
|
|
return event
|
|
})
|
|
|
|
splashScreen := tview.NewTextView()
|
|
splashScreen.SetTextAlign(tview.AlignCenter)
|
|
splashScreen.SetText(tviewutil.Escape(splashText + "\n\nConfig lies at: " + configDir))
|
|
login.AddItem(splashScreen, 12, 0, false)
|
|
login.AddItem(tviewutil.CreateCenteredComponent(login.messageText, 66), 0, 1, false)
|
|
|
|
login.messageText.SetDynamicColors(true)
|
|
|
|
configureInputComponent(login.tokenInput)
|
|
login.tokenInput.SetTitle("Token")
|
|
login.tokenInput.SetMaskCharacter(login.tokenInputMaskRune)
|
|
configureInputComponent(login.usernameInput)
|
|
login.usernameInput.SetTitle("E-Mail")
|
|
configureInputComponent(login.passwordInput)
|
|
login.passwordInput.SetTitle("Password")
|
|
login.passwordInput.SetMaskCharacter('*')
|
|
configureInputComponent(login.tfaTokenInput)
|
|
login.tfaTokenInput.SetTitle("Two-Factor-Authentication Code")
|
|
|
|
login.tokenInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyEnter {
|
|
login.attemptLogin()
|
|
return nil
|
|
}
|
|
|
|
if event.Key() == tcell.KeyCtrlV {
|
|
content, clipError := clipboard.ReadAll()
|
|
if clipError != nil {
|
|
panic(clipError)
|
|
}
|
|
login.tokenInput.Insert(content)
|
|
}
|
|
|
|
if event.Key() == tcell.KeyCtrlR {
|
|
if login.tokenInputMasked {
|
|
login.tokenInputMasked = false
|
|
login.tokenInput.SetMaskCharacter(0)
|
|
} else {
|
|
login.tokenInputMasked = true
|
|
login.tokenInput.SetMaskCharacter(login.tokenInputMaskRune)
|
|
}
|
|
}
|
|
|
|
return event
|
|
})
|
|
|
|
login.usernameInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyEnter:
|
|
login.attemptLogin()
|
|
return nil
|
|
case tcell.KeyTAB, tcell.KeyDown:
|
|
login.app.SetFocus(login.passwordInput)
|
|
return nil
|
|
case tcell.KeyBacktab, tcell.KeyUp:
|
|
login.app.SetFocus(login.tfaTokenInput)
|
|
return nil
|
|
case tcell.KeyCtrlV:
|
|
content, clipError := clipboard.ReadAll()
|
|
if clipError != nil {
|
|
panic(clipError)
|
|
}
|
|
login.usernameInput.Insert(content)
|
|
}
|
|
return event
|
|
})
|
|
|
|
login.passwordInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyEnter:
|
|
login.attemptLogin()
|
|
return nil
|
|
case tcell.KeyTAB, tcell.KeyDown:
|
|
login.app.SetFocus(login.tfaTokenInput)
|
|
return nil
|
|
case tcell.KeyBacktab, tcell.KeyUp:
|
|
login.app.SetFocus(login.usernameInput)
|
|
return nil
|
|
case tcell.KeyCtrlV:
|
|
content, clipError := clipboard.ReadAll()
|
|
if clipError != nil {
|
|
panic(clipError)
|
|
}
|
|
login.passwordInput.Insert(content)
|
|
}
|
|
return event
|
|
})
|
|
|
|
login.tfaTokenInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Key() {
|
|
case tcell.KeyEnter:
|
|
login.attemptLogin()
|
|
return nil
|
|
case tcell.KeyTAB, tcell.KeyDown:
|
|
login.app.SetFocus(login.usernameInput)
|
|
return nil
|
|
case tcell.KeyBacktab, tcell.KeyUp:
|
|
login.app.SetFocus(login.passwordInput)
|
|
return nil
|
|
case tcell.KeyCtrlV:
|
|
content, clipError := clipboard.ReadAll()
|
|
if clipError != nil {
|
|
panic(clipError)
|
|
}
|
|
login.tfaTokenInput.Insert(content)
|
|
}
|
|
return event
|
|
})
|
|
|
|
//FIXME Mouse won't work for some reason.
|
|
|
|
login.loginTypeTokenButton.SetSelectedFunc(func() {
|
|
login.loginType = Token
|
|
login.showTokenLogin()
|
|
})
|
|
login.loginTypeTokenButton.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyTab || event.Key() == tcell.KeyBacktab || event.Key() == tcell.KeyDown ||
|
|
event.Key() == tcell.KeyUp || event.Key() == tcell.KeyLeft || event.Key() == tcell.KeyRight {
|
|
login.app.SetFocus(login.loginTypePasswordButton)
|
|
return nil
|
|
}
|
|
|
|
return event
|
|
})
|
|
|
|
login.loginTypePasswordButton.SetSelectedFunc(func() {
|
|
login.loginType = Password
|
|
login.showPasswordLogin()
|
|
})
|
|
login.loginTypePasswordButton.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
if event.Key() == tcell.KeyTab || event.Key() == tcell.KeyBacktab || event.Key() == tcell.KeyDown ||
|
|
event.Key() == tcell.KeyUp || event.Key() == tcell.KeyLeft || event.Key() == tcell.KeyRight {
|
|
login.app.SetFocus(login.loginTypeTokenButton)
|
|
return nil
|
|
}
|
|
|
|
return event
|
|
})
|
|
|
|
loginChoiceView := tview.NewFlex().SetDirection(tview.FlexRow)
|
|
loginChoiceView.AddItem(tviewutil.CreateCenteredComponent(login.loginTypeTokenButton, 66), 1, 0, true)
|
|
loginChoiceView.AddItem(tview.NewBox(), 1, 0, false)
|
|
loginChoiceView.AddItem(tviewutil.CreateCenteredComponent(tview.NewTextView().SetText("or").SetTextAlign(tview.AlignCenter), 66), 1, 0, false)
|
|
loginChoiceView.AddItem(tview.NewBox(), 1, 0, false)
|
|
loginChoiceView.AddItem(tviewutil.CreateCenteredComponent(login.loginTypePasswordButton, 66), 1, 0, false)
|
|
|
|
passwordInputView := tview.NewFlex().SetDirection(tview.FlexRow)
|
|
passwordInputView.AddItem(tviewutil.CreateCenteredComponent(login.usernameInput, 68), 3, 0, false)
|
|
passwordInputView.AddItem(tviewutil.CreateCenteredComponent(login.passwordInput, 68), 3, 0, false)
|
|
passwordInputView.AddItem(tviewutil.CreateCenteredComponent(login.tfaTokenInput, 68), 3, 0, false)
|
|
|
|
login.AddItem(login.content, 0, 0, false)
|
|
login.AddItem(tview.NewBox(), 0, 1, false)
|
|
|
|
login.loginChoiceView = loginChoiceView
|
|
login.passwordInputView = passwordInputView
|
|
login.tokenInputView = login.tokenInput
|
|
|
|
return login
|
|
}
|
|
|
|
func configureInputComponent(component *tview.InputField) {
|
|
component.SetBorder(true)
|
|
component.SetFieldWidth(66)
|
|
}
|
|
|
|
func (login *Login) attemptLogin() {
|
|
//Following two lines are little hack to prevent anything from being leftover
|
|
//in the terminal buffer from the previous view. This is a bug in the tview
|
|
//drawing logic.
|
|
login.app.SetFocus(nil)
|
|
login.ResizeItem(login.content, 0, 0)
|
|
|
|
login.messageText.SetText("Attempting to log in ...")
|
|
|
|
switch login.loginType {
|
|
case None:
|
|
panic("Was in state loginType=None during login attempt.")
|
|
case Token:
|
|
session, loginError := discordgo.NewWithToken(login.tokenInput.GetText())
|
|
login.sessionChannel <- &loginAttempt{session, loginError}
|
|
case Password:
|
|
// Even if the login is supposed to be without two-factor-authentication, we
|
|
// attempt parsing a 2fa code, since the underlying rest-call can also handle
|
|
// non-2fa login calls.
|
|
input := login.tfaTokenInput.GetText()
|
|
var mfaTokenText string
|
|
if input != "" {
|
|
var parseError error
|
|
mfaTokenText, parseError = text.ParseTFACode(input)
|
|
if parseError != nil {
|
|
login.sessionChannel <- &loginAttempt{nil, errors.New("[red]Two-Factor-Authentication Code incorrect.\n\n[red]Correct example: 564 231")}
|
|
}
|
|
}
|
|
|
|
session, loginError := discordgo.NewWithPasswordAndMFA(login.usernameInput.GetText(), login.passwordInput.GetText(), mfaTokenText)
|
|
login.sessionChannel <- &loginAttempt{session, loginError}
|
|
}
|
|
|
|
}
|
|
|
|
// RequestLogin prompts the user for a discord login. Login can either be done
|
|
// via e-mail and password or an authentication token.
|
|
func (login *Login) RequestLogin(additionalMessage string) (*discordgo.Session, error) {
|
|
switch login.loginType {
|
|
case None:
|
|
login.showLoginTypeChoice()
|
|
case Token:
|
|
login.showTokenLogin()
|
|
case Password:
|
|
login.showPasswordLogin()
|
|
}
|
|
|
|
if additionalMessage != "" {
|
|
login.messageText.SetText(additionalMessage + "\n\n" + login.messageText.GetText(false))
|
|
}
|
|
|
|
//Easier, since we don't have to call QueueUpdateDraw. Without this, we won't see any of the primitives.
|
|
login.app.ForceDraw()
|
|
|
|
loginAttempt := <-login.sessionChannel
|
|
login.loginType = None
|
|
return loginAttempt.session, loginAttempt.loginError
|
|
}
|
|
|
|
func (login *Login) showLoginTypeChoice() {
|
|
login.showView(login.loginChoiceView, 5)
|
|
login.app.SetFocus(login.loginTypeTokenButton)
|
|
login.messageText.SetText("Please choose a login method.\n\nLogging in as a Bot will only work using a Authentication-Token.")
|
|
}
|
|
|
|
func (login *Login) showPasswordLogin() {
|
|
login.showView(login.passwordInputView, 9)
|
|
login.app.SetFocus(login.usernameInput)
|
|
login.messageText.SetText("Please input your E-Mail and password.\n\nIf you haven't enabled Two-Factor-Authentication on your account, just leave the field empty.")
|
|
}
|
|
|
|
func (login *Login) showTokenLogin() {
|
|
login.showView(login.tokenInputView, 3)
|
|
login.app.SetFocus(login.tokenInput)
|
|
login.messageText.SetText("Prepend 'Bot ' for bot tokens.\n\nFor information on how to retrieve your token, check:\nhttps://github.com/Bios-Marcel/cordless/wiki/Retrieving-your-token\n\nToken input is hidden by default, toggle with Ctrl + R.")
|
|
}
|
|
|
|
func (login *Login) showView(view tview.Primitive, size int) {
|
|
login.content.RemoveAllItems()
|
|
login.ResizeItem(login.content, size, 0)
|
|
login.content.AddItem(tviewutil.CreateCenteredComponent(view, 68), size, 0, false)
|
|
}
|