refactor(ui): decouple commands from models
This commit is contained in:
parent
f4621bc321
commit
1197e4b204
7 changed files with 71 additions and 69 deletions
|
|
@ -171,7 +171,7 @@ func (c *composer) Update(msg tview.Msg) tview.Cmd {
|
|||
case tview.KeyMsg:
|
||||
switch {
|
||||
case keybind.Matches(msg, c.cfg.Keybinds.Composer.Paste.Keybind):
|
||||
return tview.Sequence(c.pasteImage(), c.forwardToTextArea(tcell.NewEventKey(tcell.KeyCtrlV, "", tcell.ModNone)))
|
||||
return tview.Sequence(pasteImage(), c.forwardToTextArea(tcell.NewEventKey(tcell.KeyCtrlV, "", tcell.ModNone)))
|
||||
case keybind.Matches(msg, c.cfg.Keybinds.Composer.Newline.Keybind):
|
||||
return c.forwardToTextArea(tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModNone))
|
||||
case keybind.Matches(msg, c.cfg.Keybinds.Composer.Send.Keybind):
|
||||
|
|
@ -220,7 +220,7 @@ func (c *composer) Update(msg tview.Msg) tview.Cmd {
|
|||
|
||||
type imagePastedMsg []byte
|
||||
|
||||
func (c *composer) pasteImage() tview.Cmd {
|
||||
func pasteImage() tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
data := clipboard.Read(clipboard.FmtImage)
|
||||
return imagePastedMsg(data)
|
||||
|
|
|
|||
|
|
@ -1069,14 +1069,14 @@ func (ml *messagesList) open() tview.Cmd {
|
|||
case total > 1:
|
||||
return ml.showAttachmentsList(urls, selectedMessage.Attachments)
|
||||
case len(urls) == 1:
|
||||
return ml.openURL(urls[0])
|
||||
return openURL(urls[0])
|
||||
}
|
||||
|
||||
attachment := selectedMessage.Attachments[0]
|
||||
if strings.HasPrefix(attachment.ContentType, "image/") {
|
||||
return ml.openAttachment(attachment)
|
||||
return openAttachment(attachment)
|
||||
}
|
||||
return ml.openURL(attachment.URL)
|
||||
return openURL(attachment.URL)
|
||||
}
|
||||
|
||||
func extractURLs(content string) []string {
|
||||
|
|
@ -1148,9 +1148,9 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
|
|||
var items []attachmentspicker.Item
|
||||
for _, a := range attachments {
|
||||
attachment := a
|
||||
open := ml.openURL(attachment.URL)
|
||||
open := openURL(attachment.URL)
|
||||
if strings.HasPrefix(attachment.ContentType, "image/") {
|
||||
open = ml.openAttachment(attachment)
|
||||
open = openAttachment(attachment)
|
||||
}
|
||||
items = append(items, attachmentspicker.Item{
|
||||
Label: attachment.Filename,
|
||||
|
|
@ -1161,7 +1161,7 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
|
|||
url := u
|
||||
items = append(items, attachmentspicker.Item{
|
||||
Label: url,
|
||||
Open: ml.openURL(url),
|
||||
Open: openURL(url),
|
||||
})
|
||||
}
|
||||
ml.attachmentsPicker.SetItems(items)
|
||||
|
|
@ -1178,7 +1178,7 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
|
|||
return tview.SetFocus(ml.attachmentsPicker)
|
||||
}
|
||||
|
||||
func (ml *messagesList) openAttachment(attachment discord.Attachment) tview.Cmd {
|
||||
func openAttachment(attachment discord.Attachment) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
resp, err := http.Get(attachment.URL)
|
||||
if err != nil {
|
||||
|
|
@ -1220,7 +1220,7 @@ func (ml *messagesList) openAttachment(attachment discord.Attachment) tview.Cmd
|
|||
}
|
||||
}
|
||||
|
||||
func (ml *messagesList) openURL(url string) tview.Cmd {
|
||||
func openURL(url string) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
if err := open.Start(url); err != nil {
|
||||
slog.Error("failed to open URL", "err", err, "url", url)
|
||||
|
|
|
|||
|
|
@ -272,17 +272,17 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
m.focused = msg.Model
|
||||
return nil
|
||||
case tview.InitMsg:
|
||||
return tview.Batch(m.openState(), m.listen())
|
||||
return tview.Batch(openState(m.state), listen(m.events))
|
||||
case gateway.Event:
|
||||
switch eventMsg := msg.(type) {
|
||||
case *ws.RawEvent:
|
||||
m.onRaw(eventMsg)
|
||||
|
||||
case *gateway.ReadyEvent:
|
||||
return tview.Batch(m.onReady(eventMsg), m.listen())
|
||||
return tview.Batch(m.onReady(eventMsg), listen(m.events))
|
||||
|
||||
case *gateway.MessageCreateEvent:
|
||||
return tview.Batch(m.onMessageCreate(eventMsg), m.listen())
|
||||
return tview.Batch(m.onMessageCreate(eventMsg), listen(m.events))
|
||||
case *gateway.MessageUpdateEvent:
|
||||
m.onMessageUpdate(eventMsg)
|
||||
case *gateway.PresenceUpdateEvent:
|
||||
|
|
@ -299,7 +299,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
m.onMessageReaction(eventMsg.ChannelID, eventMsg.MessageID)
|
||||
|
||||
case *gateway.GuildMembersChunkEvent:
|
||||
return tview.Batch(m.onGuildMembersChunk(eventMsg), m.listen())
|
||||
return tview.Batch(m.onGuildMembersChunk(eventMsg), listen(m.events))
|
||||
case *gateway.GuildMemberRemoveEvent:
|
||||
m.onGuildMemberRemove(eventMsg)
|
||||
|
||||
|
|
@ -311,7 +311,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
case *read.UpdateEvent:
|
||||
m.onReadUpdate(eventMsg)
|
||||
}
|
||||
return m.listen()
|
||||
return listen(m.events)
|
||||
case channelLoadedMsg:
|
||||
node := m.guildsTree.CurrentNode()
|
||||
if node == nil {
|
||||
|
|
@ -359,7 +359,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
case attachmentspicker.CancelMsg:
|
||||
return m.closeAttachmentsPicker()
|
||||
case QuitMsg:
|
||||
return m.closeState()
|
||||
return closeState(m.state)
|
||||
case tview.KeyMsg:
|
||||
switch {
|
||||
case keybind.Matches(msg, m.cfg.Keybinds.FocusGuildsTree.Keybind):
|
||||
|
|
@ -382,7 +382,7 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
return m.togglePicker()
|
||||
|
||||
case keybind.Matches(msg, m.cfg.Keybinds.Logout.Keybind):
|
||||
return tview.Sequence(m.closeState(), m.logout())
|
||||
return tview.Sequence(closeState(m.state), logout())
|
||||
}
|
||||
case tabSuggestMsg:
|
||||
return m.composer.Update(msg)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import (
|
|||
"log/slog"
|
||||
|
||||
"github.com/ayn2op/arikawa/v3/discord"
|
||||
"github.com/ayn2op/arikawa/v3/gateway"
|
||||
"github.com/ayn2op/ningen/v3"
|
||||
"github.com/ayn2op/tview"
|
||||
)
|
||||
|
||||
func (m *Model) openState() tview.Cmd {
|
||||
func openState(state *ningen.State) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
if err := m.state.Open(context.Background()); err != nil {
|
||||
if err := state.Open(context.Background()); err != nil {
|
||||
slog.Error("failed to open chat state", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -18,21 +20,21 @@ func (m *Model) openState() tview.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *Model) closeState() tview.Cmd {
|
||||
if m.state == nil {
|
||||
func closeState(state *ningen.State) tview.Cmd {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
return func() tview.Msg {
|
||||
if err := m.state.Close(); err != nil {
|
||||
if err := state.Close(); err != nil {
|
||||
slog.Error("failed to close the session", "err", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) listen() tview.Cmd {
|
||||
func listen(events <-chan gateway.Event) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
return <-m.events
|
||||
return <-events
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +52,7 @@ type deleteMessageMsg discord.Message
|
|||
|
||||
type LogoutMsg struct{}
|
||||
|
||||
func (m *Model) logout() tview.Cmd {
|
||||
func logout() tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
return LogoutMsg{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,16 +39,16 @@ 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)
|
||||
return showErrorDialog(msg)
|
||||
case password.ErrMsg:
|
||||
return m.showErrorDialog(msg)
|
||||
return showErrorDialog(msg)
|
||||
case copyErrorMsg:
|
||||
return setClipboard(string(msg))
|
||||
}
|
||||
return m.Layers.Update(msg)
|
||||
}
|
||||
|
||||
func (m *Model) showErrorDialog(err error) tview.Cmd {
|
||||
func showErrorDialog(err error) tview.Cmd {
|
||||
slog.Error("failed to login", "err", err)
|
||||
message := err.Error()
|
||||
return ui.ShowModal(message,
|
||||
|
|
|
|||
|
|
@ -47,62 +47,62 @@ func (m *Model) Update(msg tview.Msg) tview.Cmd {
|
|||
switch msg := msg.(type) {
|
||||
case tview.InitMsg:
|
||||
m.setStatus("Connecting to Remote Auth Gateway...")
|
||||
return m.connect()
|
||||
return connect()
|
||||
case tview.KeyMsg:
|
||||
if msg.Key() == tcell.KeyEsc {
|
||||
m.setStatus("Canceled")
|
||||
return m.close()
|
||||
return closeConn(m.conn)
|
||||
}
|
||||
return m.TextView.Update(msg)
|
||||
|
||||
case connCreateMsg:
|
||||
m.conn = msg.conn
|
||||
m.setStatus("Connected. Handshaking...")
|
||||
return m.listen()
|
||||
return listen(m.conn)
|
||||
case connCloseMsg:
|
||||
m.conn = nil
|
||||
return nil
|
||||
|
||||
case helloMsg:
|
||||
m.heartbeatInterval = time.Duration(msg.heartbeatInterval) * time.Millisecond
|
||||
return tview.Batch(m.listen(), m.heartbeat(), m.generatePrivateKey())
|
||||
return tview.Batch(listen(m.conn), heartbeat(m.heartbeatInterval), generatePrivateKey())
|
||||
case privateKeyMsg:
|
||||
m.privateKey = msg.privateKey
|
||||
return tview.Batch(m.listen(), m.sendInit())
|
||||
return tview.Batch(listen(m.conn), sendInit(m.conn, m.privateKey))
|
||||
case nonceProofMsg:
|
||||
return tview.Batch(m.listen(), m.sendNonceProof(msg.encryptedNonce))
|
||||
return tview.Batch(listen(m.conn), sendNonceProof(m.conn, m.privateKey, msg.encryptedNonce))
|
||||
case pendingRemoteInitMsg:
|
||||
m.fingerprint = msg.fingerprint
|
||||
return tview.Batch(m.listen(), m.generateQRCode(msg.fingerprint))
|
||||
return tview.Batch(listen(m.conn), generateQRCode(msg.fingerprint))
|
||||
case qrCodeMsg:
|
||||
m.qrCode = msg.qrCode
|
||||
m.setStatus("Scan this with the Discord mobile app to log in instantly.")
|
||||
return m.listen()
|
||||
return listen(m.conn)
|
||||
case pendingTicketMsg:
|
||||
return tview.Batch(m.listen(), m.decryptUserPayload(msg.encryptedUserPayload))
|
||||
return tview.Batch(listen(m.conn), decryptUserPayload(m.privateKey, msg.encryptedUserPayload))
|
||||
case userMsg:
|
||||
name := msg.username
|
||||
if msg.discriminator != "0" {
|
||||
name += "#" + msg.discriminator
|
||||
}
|
||||
m.setStatus("Check your phone! Logging in as " + name)
|
||||
return m.listen()
|
||||
return listen(m.conn)
|
||||
case pendingLoginMsg:
|
||||
m.setStatus("Authenticating...")
|
||||
return tview.Batch(m.close(), m.exchangeTicket(msg.ticket))
|
||||
return tview.Batch(closeConn(m.conn), exchangeTicket(m.fingerprint, m.privateKey, msg.ticket))
|
||||
case cancelMsg:
|
||||
m.setStatus("Login canceled on mobile")
|
||||
return m.close()
|
||||
return closeConn(m.conn)
|
||||
|
||||
case heartbeatTickMsg:
|
||||
if m.conn == nil {
|
||||
return nil
|
||||
}
|
||||
return tview.Batch(m.heartbeat(), m.sendHeartbeat())
|
||||
return tview.Batch(heartbeat(m.heartbeatInterval), sendHeartbeat(m.conn))
|
||||
|
||||
case errMsg:
|
||||
m.setStatus(msg.Error())
|
||||
return m.close()
|
||||
return closeConn(m.conn)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ type connCreateMsg struct {
|
|||
|
||||
type connCloseMsg struct{}
|
||||
|
||||
func (m *Model) connect() tview.Cmd {
|
||||
func connect() tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
headers := http.Headers()
|
||||
headers.Set("User-Agent", http.BrowserUserAgent())
|
||||
|
|
@ -45,10 +45,10 @@ func (m *Model) connect() tview.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *Model) close() tview.Cmd {
|
||||
func closeConn(conn *websocket.Conn) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
if m.conn != nil {
|
||||
if err := m.conn.Close(); err != nil {
|
||||
if conn != nil {
|
||||
if err := conn.Close(); err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -79,13 +79,13 @@ type pendingLoginMsg struct {
|
|||
|
||||
type cancelMsg struct{}
|
||||
|
||||
func (m *Model) listen() tview.Cmd {
|
||||
func listen(conn *websocket.Conn) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
if m.conn == nil {
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, data, err := m.conn.ReadMessage()
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
|
|
@ -149,22 +149,22 @@ func (m *Model) listen() tview.Cmd {
|
|||
|
||||
type heartbeatTickMsg struct{}
|
||||
|
||||
func (m *Model) heartbeat() tview.Cmd {
|
||||
func heartbeat(interval time.Duration) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
time.Sleep(m.heartbeatInterval)
|
||||
time.Sleep(interval)
|
||||
return heartbeatTickMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) sendHeartbeat() tview.Cmd {
|
||||
func sendHeartbeat(conn *websocket.Conn) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
if m.conn == nil {
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
data := struct {
|
||||
Op string `json:"op"`
|
||||
}{"heartbeat"}
|
||||
if err := m.conn.WriteJSON(data); err != nil {
|
||||
if err := conn.WriteJSON(data); err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -175,7 +175,7 @@ type privateKeyMsg struct {
|
|||
privateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (m *Model) generatePrivateKey() tview.Cmd {
|
||||
func generatePrivateKey() tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
|
|
@ -185,12 +185,12 @@ func (m *Model) generatePrivateKey() tview.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *Model) sendInit() tview.Cmd {
|
||||
func sendInit(conn *websocket.Conn, privateKey *rsa.PrivateKey) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
if m.privateKey == nil {
|
||||
if privateKey == nil {
|
||||
return errMsg(errors.New("missing private key"))
|
||||
}
|
||||
spki, err := x509.MarshalPKIXPublicKey(m.privateKey.Public())
|
||||
spki, err := x509.MarshalPKIXPublicKey(privateKey.Public())
|
||||
if err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
|
|
@ -199,21 +199,21 @@ func (m *Model) sendInit() tview.Cmd {
|
|||
Op string `json:"op"`
|
||||
EncodedPublicKey string `json:"encoded_public_key"`
|
||||
}{"init", encodedPublicKey}
|
||||
if err := m.conn.WriteJSON(data); err != nil {
|
||||
if err := conn.WriteJSON(data); err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) sendNonceProof(encryptedNonce string) tview.Cmd {
|
||||
func sendNonceProof(conn *websocket.Conn, privateKey *rsa.PrivateKey, encryptedNonce string) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
decodedNonce, err := base64.StdEncoding.DecodeString(encryptedNonce)
|
||||
if err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
decryptedNonce, err := rsa.DecryptOAEP(sha256.New(), nil, m.privateKey, decodedNonce, nil)
|
||||
decryptedNonce, err := rsa.DecryptOAEP(sha256.New(), nil, privateKey, decodedNonce, nil)
|
||||
if err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
|
|
@ -223,7 +223,7 @@ func (m *Model) sendNonceProof(encryptedNonce string) tview.Cmd {
|
|||
Op string `json:"op"`
|
||||
Nonce string `json:"nonce"`
|
||||
}{"nonce_proof", encodedNonce}
|
||||
if err := m.conn.WriteJSON(data); err != nil {
|
||||
if err := conn.WriteJSON(data); err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -234,7 +234,7 @@ type qrCodeMsg struct {
|
|||
qrCode *qrcode.QRCode
|
||||
}
|
||||
|
||||
func (m *Model) generateQRCode(fingerprint string) tview.Cmd {
|
||||
func generateQRCode(fingerprint string) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
content := "https://discord.com/ra/" + fingerprint
|
||||
qrCode, err := qrcode.New(content, qrcode.Low)
|
||||
|
|
@ -251,14 +251,14 @@ type userMsg struct {
|
|||
username string
|
||||
}
|
||||
|
||||
func (m *Model) decryptUserPayload(encryptedPayload string) tview.Cmd {
|
||||
func decryptUserPayload(privateKey *rsa.PrivateKey, encryptedPayload string) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
decodedPayload, err := base64.StdEncoding.DecodeString(encryptedPayload)
|
||||
if err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
|
||||
decryptedPayload, err := rsa.DecryptOAEP(sha256.New(), nil, m.privateKey, decodedPayload, nil)
|
||||
decryptedPayload, err := rsa.DecryptOAEP(sha256.New(), nil, privateKey, decodedPayload, nil)
|
||||
if err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
|
|
@ -272,12 +272,12 @@ func (m *Model) decryptUserPayload(encryptedPayload string) tview.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *Model) exchangeTicket(ticket string) tview.Cmd {
|
||||
func exchangeTicket(fingerprint string, privateKey *rsa.PrivateKey, ticket string) tview.Cmd {
|
||||
return func() tview.Msg {
|
||||
headers := http.Headers()
|
||||
headers.Set("Referer", "https://discord.com/login")
|
||||
if m.fingerprint != "" {
|
||||
headers.Set("X-Fingerprint", m.fingerprint)
|
||||
if fingerprint != "" {
|
||||
headers.Set("X-Fingerprint", fingerprint)
|
||||
}
|
||||
|
||||
client := http.NewClient("")
|
||||
|
|
@ -293,7 +293,7 @@ func (m *Model) exchangeTicket(ticket string) tview.Cmd {
|
|||
return errMsg(err)
|
||||
}
|
||||
|
||||
decryptedToken, err := rsa.DecryptOAEP(sha256.New(), nil, m.privateKey, decodedToken, nil)
|
||||
decryptedToken, err := rsa.DecryptOAEP(sha256.New(), nil, privateKey, decodedToken, nil)
|
||||
if err != nil {
|
||||
return errMsg(err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue