model.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. package qr
  2. import (
  3. "crypto/rsa"
  4. "fmt"
  5. "strings"
  6. "time"
  7. "github.com/ayn2op/tview"
  8. "github.com/ayn2op/tview/tabs"
  9. "github.com/gdamore/tcell/v3"
  10. "github.com/gorilla/websocket"
  11. "github.com/skip2/go-qrcode"
  12. )
  13. type Model struct {
  14. *tview.TextView
  15. app *tview.Application
  16. conn *websocket.Conn
  17. heartbeatInterval time.Duration
  18. privateKey *rsa.PrivateKey
  19. fingerprint string
  20. qrCode *qrcode.QRCode
  21. msg string
  22. }
  23. func NewModel(app *tview.Application) *Model {
  24. m := &Model{
  25. TextView: tview.NewTextView(),
  26. app: app,
  27. }
  28. m.
  29. SetScrollable(true).
  30. SetWrap(false).
  31. SetTextAlign(tview.AlignmentCenter).
  32. SetChangedFunc(func() {
  33. m.app.QueueUpdateDraw(func() {})
  34. })
  35. m.msg = "Press Ctrl+N to open QR login"
  36. return m
  37. }
  38. var _ tabs.Tab = (*Model)(nil)
  39. func (m *Model) Label() string {
  40. return "QR"
  41. }
  42. func (m *Model) HandleEvent(event tcell.Event) tview.Command {
  43. switch event := event.(type) {
  44. case *tview.InitEvent:
  45. m.msg = "Connecting to Remote Auth Gateway..."
  46. return m.connect()
  47. case *tview.KeyEvent:
  48. if event.Key() == tcell.KeyEsc {
  49. m.msg = "Canceled"
  50. return tview.BatchCommand{m.close(), tview.RedrawCommand{}}
  51. }
  52. return m.TextView.HandleEvent(event)
  53. case *connCreateEvent:
  54. m.conn = event.conn
  55. m.msg = "Connected. Handshaking..."
  56. return m.listen()
  57. case *connCloseEvent:
  58. m.conn = nil
  59. return nil
  60. case *helloEvent:
  61. m.heartbeatInterval = time.Duration(event.heartbeatInterval) * time.Millisecond
  62. return tview.BatchCommand{m.listen(), m.heartbeat(), m.generatePrivateKey()}
  63. case *privateKeyEvent:
  64. m.privateKey = event.privateKey
  65. return tview.BatchCommand{m.listen(), m.sendInit()}
  66. case *nonceProofEvent:
  67. return tview.BatchCommand{m.listen(), m.sendNonceProof(event.encryptedNonce)}
  68. case *pendingRemoteInitEvent:
  69. m.fingerprint = event.fingerprint
  70. return tview.BatchCommand{m.listen(), m.generateQRCode(event.fingerprint)}
  71. case *qrCodeEvent:
  72. m.qrCode = event.qrCode
  73. m.msg = "Scan this with the Discord mobile app to log in instantly."
  74. return m.listen()
  75. case *pendingTicketEvent:
  76. return tview.BatchCommand{m.listen(), m.decryptUserPayload(event.encryptedUserPayload)}
  77. case *userEvent:
  78. name := event.username
  79. if event.discriminator != "0" {
  80. name += "#" + event.discriminator
  81. }
  82. m.msg = fmt.Sprintf("Check your phone! Logging in as %s", name)
  83. return m.listen()
  84. case *pendingLoginEvent:
  85. m.msg = "Authenticating..."
  86. return tview.BatchCommand{m.close(), m.exchangeTicket(event.ticket)}
  87. case *cancelEvent:
  88. m.msg = "Login canceled on mobile"
  89. return m.close()
  90. case *heartbeatTickEvent:
  91. if m.conn == nil {
  92. return nil
  93. }
  94. return tview.BatchCommand{m.heartbeat(), m.sendHeartbeat()}
  95. case *tcell.EventError:
  96. m.msg = event.Error()
  97. return tview.BatchCommand{m.close(), event}
  98. }
  99. return nil
  100. }
  101. func (m *Model) Draw(screen tcell.Screen) {
  102. var contents []string
  103. if m.qrCode != nil {
  104. bitmap := m.qrCode.Bitmap()
  105. var b strings.Builder
  106. for y := 0; y < len(bitmap); y += 2 {
  107. for x := range bitmap[y] {
  108. top := bitmap[y][x]
  109. bottom := false
  110. if y+1 < len(bitmap) {
  111. bottom = bitmap[y+1][x]
  112. }
  113. if top && bottom {
  114. b.WriteString("█")
  115. } else if top && !bottom {
  116. b.WriteString("▀")
  117. } else if !top && bottom {
  118. b.WriteString("▄")
  119. } else {
  120. b.WriteByte(' ')
  121. }
  122. }
  123. b.WriteByte('\n')
  124. }
  125. contents = append(contents, b.String())
  126. }
  127. if m.msg != "" {
  128. contents = append(contents, m.msg)
  129. }
  130. builder := tview.NewLineBuilder()
  131. builder.Write(strings.Join(contents, "\n"), tcell.StyleDefault)
  132. m.SetLines(m.centerLines(builder.Finish()))
  133. m.TextView.Draw(screen)
  134. }
  135. func (m *Model) centerLines(lines []tview.Line) []tview.Line {
  136. _, _, _, height := m.GetInnerRect()
  137. if height == 0 {
  138. height = 40
  139. }
  140. padding := (height - len(lines)) / 2
  141. if padding < 0 {
  142. padding = 0
  143. } else if padding < 1 && height > len(lines) {
  144. padding = 1
  145. }
  146. if padding == 0 {
  147. return lines
  148. }
  149. centered := make([]tview.Line, 0, padding+len(lines))
  150. centered = append(centered, make([]tview.Line, padding)...)
  151. centered = append(centered, lines...)
  152. return centered
  153. }