feat(ui): add password login
This commit is contained in:
parent
16b4465fb9
commit
228bfaeb05
10 changed files with 129 additions and 51 deletions
24
README.md
24
README.md
|
|
@ -40,7 +40,21 @@ go build .
|
|||
|
||||
## Usage
|
||||
|
||||
### Token (UI, recommended)
|
||||
### Password (UI, recommended)
|
||||
|
||||
1. Run the `discordo` executable with no arguments.
|
||||
|
||||
2. Open the "Password" tab, enter your email address or E.164-formatted phone number and password, then click the "Login" button.
|
||||
|
||||
### QR (UI)
|
||||
|
||||
1. Run the `discordo` executable with no arguments.
|
||||
|
||||
2. Click on the "Login with QR" button.
|
||||
|
||||
3. Follow the instructions in the QR Login screen.
|
||||
|
||||
### Token (UI)
|
||||
|
||||
1. Run the `discordo` executable with no arguments.
|
||||
|
||||
|
|
@ -54,14 +68,6 @@ Set the value of the `DISCORDO_TOKEN` environment variable to the authentication
|
|||
DISCORDO_TOKEN="OTI2MDU5NTQxNDE2Nzc5ODA2.Yc2KKA.2iZ-5JxgxG-9Ub8GHzBSn-NJjNg" discordo
|
||||
```
|
||||
|
||||
### QR (UI)
|
||||
|
||||
1. Run the `discordo` executable with no arguments.
|
||||
|
||||
2. Click on the "Login with QR" button.
|
||||
|
||||
3. Follow the instructions in the QR Login screen.
|
||||
|
||||
## Configuration
|
||||
|
||||
The configuration file allows you to configure and customize the behavior, keybindings, and theme of the application.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/ayn2op/discordo/internal/config"
|
||||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/discordo/internal/ui/login/password"
|
||||
"github.com/ayn2op/discordo/internal/ui/login/qr"
|
||||
"github.com/ayn2op/discordo/internal/ui/login/token"
|
||||
"github.com/ayn2op/tview"
|
||||
|
|
@ -23,7 +24,7 @@ type Model struct {
|
|||
}
|
||||
|
||||
func NewModel(cfg *config.Config) *Model {
|
||||
tabs := tabs.NewModel([]tabs.Tab{token.NewModel(), qr.NewModel()})
|
||||
tabs := tabs.NewModel([]tabs.Tab{password.NewModel(), qr.NewModel(), token.NewModel()})
|
||||
|
||||
l := layers.New()
|
||||
ui.ConfigureBox(l.Box, &cfg.Theme)
|
||||
|
|
@ -38,7 +39,9 @@ func NewModel(cfg *config.Config) *Model {
|
|||
func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
||||
switch msg := msg.(type) {
|
||||
case errMsg:
|
||||
return m.showErrorDialog(msg.err)
|
||||
return m.showErrorDialog(msg)
|
||||
case password.ErrMsg:
|
||||
return m.showErrorDialog(msg)
|
||||
case copyErrorMsg:
|
||||
return setClipboard(string(msg))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import (
|
|||
"golang.design/x/clipboard"
|
||||
)
|
||||
|
||||
type errMsg struct {
|
||||
err error
|
||||
}
|
||||
type errMsg error
|
||||
|
||||
type copyErrorMsg string
|
||||
|
||||
|
|
|
|||
37
internal/ui/login/password/model.go
Normal file
37
internal/ui/login/password/model.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package password
|
||||
|
||||
import (
|
||||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/tabs"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
*tview.Form
|
||||
}
|
||||
|
||||
func NewModel() *Model {
|
||||
form := tview.NewForm().
|
||||
AddInputField("Login", "", 0).
|
||||
AddPasswordField("Password", "", 0, 0).
|
||||
AddButton("Login")
|
||||
return &Model{Form: form}
|
||||
}
|
||||
|
||||
var _ tabs.Tab = (*Model)(nil)
|
||||
|
||||
func (m *Model) Label() string {
|
||||
return "Password"
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
||||
switch msg.(type) {
|
||||
case tview.FormSubmitMsg:
|
||||
login := m.GetFormItem(0).(*tview.InputField).Text()
|
||||
password := m.GetFormItem(1).(*tview.InputField).Text()
|
||||
if login == "" || password == "" {
|
||||
return nil
|
||||
}
|
||||
return loginCmd(login, password)
|
||||
}
|
||||
return m.Form.Update(msg)
|
||||
}
|
||||
36
internal/ui/login/password/msg.go
Normal file
36
internal/ui/login/password/msg.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package password
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ayn2op/arikawa/v3/utils/httputil"
|
||||
"github.com/ayn2op/discordo/internal/http"
|
||||
"github.com/ayn2op/tview"
|
||||
)
|
||||
|
||||
type (
|
||||
TokenMsg string
|
||||
ErrMsg error
|
||||
)
|
||||
|
||||
func loginCmd(login, password string) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
headers := http.Headers()
|
||||
headers.Set("Referer", "https://discord.com/login")
|
||||
|
||||
client := http.NewClient("")
|
||||
client.OnRequest = append(client.OnRequest, httputil.WithHeaders(headers))
|
||||
|
||||
loginResp, err := client.Login(login, password)
|
||||
if err != nil {
|
||||
return ErrMsg(err)
|
||||
}
|
||||
if loginResp.Token == "" {
|
||||
if loginResp.MFA {
|
||||
return ErrMsg(errors.New("multi-factor authentication is required; use token or QR login"))
|
||||
}
|
||||
return ErrMsg(errors.New("login response did not include a token"))
|
||||
}
|
||||
return TokenMsg(loginResp.Token)
|
||||
}
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
return tview.Batch(m.heartbeat(), m.sendHeartbeat())
|
||||
|
||||
case errMsg:
|
||||
m.msg = msg.err.Error()
|
||||
m.msg = msg.Error()
|
||||
return m.close()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,15 +19,10 @@ import (
|
|||
"github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
type errMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func newErrMsg(err error) errMsg {
|
||||
return errMsg{err: err}
|
||||
}
|
||||
|
||||
type TokenMsg string
|
||||
type (
|
||||
TokenMsg string
|
||||
errMsg error
|
||||
)
|
||||
|
||||
const remoteAuthGatewayURL = "wss://remote-auth-gateway.discord.gg/?v=2"
|
||||
|
||||
|
|
@ -44,7 +39,7 @@ func (m *Model) connect() tview.Cmd {
|
|||
dialer := gateway.NewDialer()
|
||||
conn, _, err := dialer.Dial(remoteAuthGatewayURL, headers)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return connCreateMsg{conn: conn}
|
||||
}
|
||||
|
|
@ -54,7 +49,7 @@ func (m *Model) close() tview.Cmd {
|
|||
return func() tview.Msg {
|
||||
if m.conn != nil {
|
||||
if err := m.conn.Close(); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
}
|
||||
return connCloseMsg{}
|
||||
|
|
@ -92,14 +87,14 @@ func (m *Model) listen() tview.Cmd {
|
|||
|
||||
_, data, err := m.conn.ReadMessage()
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Op string `json:"op"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
switch payload.Op {
|
||||
|
|
@ -109,7 +104,7 @@ func (m *Model) listen() tview.Cmd {
|
|||
TimeoutMS int `json:"timeout_ms"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return helloMsg{heartbeatInterval: payload.HeartbeatInterval, timeoutMS: payload.TimeoutMS}
|
||||
case "nonce_proof":
|
||||
|
|
@ -117,7 +112,7 @@ func (m *Model) listen() tview.Cmd {
|
|||
EncryptedNonce string `json:"encrypted_nonce"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return nonceProofMsg{encryptedNonce: payload.EncryptedNonce}
|
||||
case "pending_remote_init":
|
||||
|
|
@ -125,7 +120,7 @@ func (m *Model) listen() tview.Cmd {
|
|||
Fingerprint string `json:"fingerprint"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return pendingRemoteInitMsg{fingerprint: payload.Fingerprint}
|
||||
case "pending_ticket":
|
||||
|
|
@ -133,7 +128,7 @@ func (m *Model) listen() tview.Cmd {
|
|||
EncryptedUserPayload string `json:"encrypted_user_payload"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return pendingTicketMsg{encryptedUserPayload: payload.EncryptedUserPayload}
|
||||
case "cancel":
|
||||
|
|
@ -143,7 +138,7 @@ func (m *Model) listen() tview.Cmd {
|
|||
Ticket string `json:"ticket"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return pendingLoginMsg{ticket: payload.Ticket}
|
||||
default:
|
||||
|
|
@ -170,7 +165,7 @@ func (m *Model) sendHeartbeat() tview.Cmd {
|
|||
Op string `json:"op"`
|
||||
}{"heartbeat"}
|
||||
if err := m.conn.WriteJSON(data); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -184,7 +179,7 @@ func (m *Model) generatePrivateKey() tview.Cmd {
|
|||
return func() tview.Msg {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return privateKeyMsg{privateKey: privateKey}
|
||||
}
|
||||
|
|
@ -193,11 +188,11 @@ func (m *Model) generatePrivateKey() tview.Cmd {
|
|||
func (m *Model) sendInit() tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
if m.privateKey == nil {
|
||||
return newErrMsg(errors.New("missing private key"))
|
||||
return errMsg(errors.New("missing private key"))
|
||||
}
|
||||
spki, err := x509.MarshalPKIXPublicKey(m.privateKey.Public())
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
encodedPublicKey := base64.StdEncoding.EncodeToString(spki)
|
||||
data := struct {
|
||||
|
|
@ -205,7 +200,7 @@ func (m *Model) sendInit() tview.Cmd {
|
|||
EncodedPublicKey string `json:"encoded_public_key"`
|
||||
}{"init", encodedPublicKey}
|
||||
if err := m.conn.WriteJSON(data); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -215,12 +210,12 @@ func (m *Model) sendNonceProof(encryptedNonce string) tview.Cmd {
|
|||
return func() tview.Msg {
|
||||
decodedNonce, err := base64.StdEncoding.DecodeString(encryptedNonce)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
decryptedNonce, err := rsa.DecryptOAEP(sha256.New(), nil, m.privateKey, decodedNonce, nil)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
encodedNonce := base64.RawURLEncoding.EncodeToString(decryptedNonce)
|
||||
|
|
@ -229,7 +224,7 @@ func (m *Model) sendNonceProof(encryptedNonce string) tview.Cmd {
|
|||
Nonce string `json:"nonce"`
|
||||
}{"nonce_proof", encodedNonce}
|
||||
if err := m.conn.WriteJSON(data); err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -244,7 +239,7 @@ func (m *Model) generateQRCode(fingerprint string) tview.Cmd {
|
|||
content := "https://discord.com/ra/" + fingerprint
|
||||
qrCode, err := qrcode.New(content, qrcode.Low)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
qrCode.DisableBorder = true
|
||||
return qrCodeMsg{qrCode: qrCode}
|
||||
|
|
@ -260,17 +255,17 @@ func (m *Model) decryptUserPayload(encryptedPayload string) tview.Cmd {
|
|||
return func() tview.Msg {
|
||||
decodedPayload, err := base64.StdEncoding.DecodeString(encryptedPayload)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
decryptedPayload, err := rsa.DecryptOAEP(sha256.New(), nil, m.privateKey, decodedPayload, nil)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
parts := strings.Split(string(decryptedPayload), ":")
|
||||
if len(parts) != 4 {
|
||||
return newErrMsg(errors.New("invalid user payload"))
|
||||
return errMsg(errors.New("invalid user payload"))
|
||||
}
|
||||
|
||||
return userMsg{discriminator: parts[1], username: parts[3]}
|
||||
|
|
@ -290,17 +285,17 @@ func (m *Model) exchangeTicket(ticket string) tview.Cmd {
|
|||
|
||||
encryptedToken, err := client.ExchangeRemoteAuthTicket(ticket)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
decodedToken, err := base64.StdEncoding.DecodeString(encryptedToken)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
decryptedToken, err := rsa.DecryptOAEP(sha256.New(), nil, m.privateKey, decodedToken, nil)
|
||||
if err != nil {
|
||||
return newErrMsg(err)
|
||||
return errMsg(err)
|
||||
}
|
||||
return TokenMsg(string(decryptedToken))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
if token == "" {
|
||||
return nil
|
||||
}
|
||||
return tokenCommand(token)
|
||||
return tokenCmd(token)
|
||||
}
|
||||
return m.Form.Update(msg)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
type TokenMsg string
|
||||
|
||||
func tokenCommand(token string) tview.Cmd {
|
||||
func tokenCmd(token string) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
return TokenMsg(token)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/ayn2op/discordo/internal/ui"
|
||||
"github.com/ayn2op/discordo/internal/ui/chat"
|
||||
"github.com/ayn2op/discordo/internal/ui/login"
|
||||
"github.com/ayn2op/discordo/internal/ui/login/password"
|
||||
"github.com/ayn2op/discordo/internal/ui/login/qr"
|
||||
"github.com/ayn2op/discordo/internal/ui/login/token"
|
||||
"github.com/ayn2op/tview"
|
||||
|
|
@ -115,10 +116,12 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
case tokenMsg:
|
||||
return m.showChat(string(msg))
|
||||
|
||||
case token.TokenMsg:
|
||||
case password.TokenMsg:
|
||||
return tview.Batch(m.showChat(string(msg)), setToken(string(msg)))
|
||||
case qr.TokenMsg:
|
||||
return tview.Batch(m.showChat(string(msg)), setToken(string(msg)))
|
||||
case token.TokenMsg:
|
||||
return tview.Batch(m.showChat(string(msg)), setToken(string(msg)))
|
||||
|
||||
case chat.LogoutMsg:
|
||||
return tview.Batch(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue