discordo.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. package main
  2. import (
  3. "sort"
  4. "strings"
  5. "github.com/99designs/keyring"
  6. "github.com/atotto/clipboard"
  7. "github.com/diamondburned/arikawa/v3/api"
  8. "github.com/diamondburned/arikawa/v3/discord"
  9. "github.com/diamondburned/arikawa/v3/gateway"
  10. "github.com/diamondburned/arikawa/v3/session"
  11. "github.com/gdamore/tcell/v2"
  12. "github.com/rigormorrtiss/discordo/ui"
  13. "github.com/rigormorrtiss/discordo/util"
  14. "github.com/rivo/tview"
  15. )
  16. var (
  17. app *tview.Application
  18. loginForm *tview.Form
  19. guildsTreeView *tview.TreeView
  20. messagesTextView *tview.TextView
  21. messageInputField *tview.InputField
  22. mainFlex *tview.Flex
  23. kr keyring.Keyring
  24. conf *util.Config
  25. discordSession *session.Session
  26. clientID discord.UserID
  27. currentGuild gateway.GuildCreateEvent
  28. currentChannel discord.Channel
  29. )
  30. func main() {
  31. tview.Borders.HorizontalFocus = tview.Borders.Horizontal
  32. tview.Borders.VerticalFocus = tview.Borders.Vertical
  33. tview.Borders.TopLeftFocus = tview.Borders.TopLeft
  34. tview.Borders.TopRightFocus = tview.Borders.TopRight
  35. tview.Borders.BottomLeftFocus = tview.Borders.BottomLeft
  36. tview.Borders.BottomRightFocus = tview.Borders.BottomRight
  37. tview.Borders.Horizontal = ' '
  38. tview.Borders.Vertical = ' '
  39. tview.Borders.TopLeft = ' '
  40. tview.Borders.TopRight = ' '
  41. tview.Borders.BottomLeft = ' '
  42. tview.Borders.BottomRight = ' '
  43. kr = util.OpenKeyringBackend()
  44. conf = util.NewConfig()
  45. app = ui.NewApp(onAppInputCapture)
  46. guildsTreeView = ui.NewGuildsTreeView(onGuildsTreeViewSelected, conf.Theme)
  47. messagesTextView = ui.NewMessagesTextView(app, conf.Theme)
  48. messageInputField = ui.NewMessageInputField(onMessageInputFieldInputCapture, conf.Theme)
  49. mainFlex = ui.NewMainFlex(guildsTreeView, messagesTextView, messageInputField)
  50. if t := util.GetItem(kr, "token"); t != "" {
  51. app.
  52. SetRoot(mainFlex, true).
  53. SetFocus(guildsTreeView)
  54. discordSession = newSession(t)
  55. } else {
  56. loginForm = ui.NewLoginForm(onLoginFormLoginButtonSelected)
  57. app.SetRoot(loginForm, true)
  58. }
  59. if err := app.Run(); err != nil {
  60. panic(err)
  61. }
  62. }
  63. func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey {
  64. switch e.Name() {
  65. case "Alt+Rune[1]":
  66. app.SetFocus(guildsTreeView)
  67. case "Alt+Rune[2]":
  68. app.SetFocus(messagesTextView)
  69. case "Alt+Rune[3]":
  70. if messageInputField != nil {
  71. app.SetFocus(messageInputField)
  72. }
  73. }
  74. return e
  75. }
  76. func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
  77. switch e.Key() {
  78. case tcell.KeyEnter:
  79. t := strings.TrimSpace(messageInputField.GetText())
  80. if t == "" {
  81. return nil
  82. }
  83. discordSession.SendMessage(currentChannel.ID, t)
  84. messageInputField.SetText("")
  85. case tcell.KeyCtrlV:
  86. text, _ := clipboard.ReadAll()
  87. text = messageInputField.GetText() + text
  88. messageInputField.SetText(text)
  89. }
  90. return e
  91. }
  92. func newSession(token string) (s *session.Session) {
  93. api.UserAgent = "" +
  94. "Mozilla/5.0 (X11; Linux x86_64) " +
  95. "AppleWebKit/537.36 (KHTML, like Gecko) " +
  96. "Chrome/92.0.4515.131 Safari/537.36"
  97. gateway.DefaultIdentity.Browser = "Chrome"
  98. gateway.DefaultIdentity.OS = "Linux"
  99. gateway.DefaultIdentity.Device = ""
  100. var err error
  101. s, err = session.New(token)
  102. if err != nil {
  103. panic(err)
  104. }
  105. s.AddHandler(onSessionReady)
  106. s.AddHandler(onSessionMessageCreate)
  107. if err = s.Open(s.Context()); err != nil {
  108. panic(err)
  109. }
  110. return
  111. }
  112. func onSessionMessageCreate(m *gateway.MessageCreateEvent) {
  113. if currentChannel.ID == m.ChannelID {
  114. util.WriteMessage(messagesTextView, clientID, m.Message)
  115. }
  116. }
  117. func onSessionReady(r *gateway.ReadyEvent) {
  118. clientID = r.User.ID
  119. for _, g := range r.Guilds {
  120. gn := tview.NewTreeNode(g.Name).
  121. SetReference(g).
  122. Collapse()
  123. guildsTreeView.GetRoot().AddChild(gn)
  124. sort.Slice(g.Channels, func(i, j int) bool {
  125. return g.Channels[i].Position < g.Channels[j].Position
  126. })
  127. for _, c := range g.Channels {
  128. switch c.Type {
  129. case discord.GuildCategory:
  130. cn := tview.NewTreeNode(c.Name).
  131. SetReference(c)
  132. gn.AddChild(cn)
  133. case discord.GuildText, discord.GuildNews:
  134. if c.ParentID == 0 || c.ParentID == discord.NullChannelID {
  135. cn := tview.NewTreeNode("[::d]#" + c.Name + "[::-]").
  136. SetReference(c)
  137. gn.AddChild(cn)
  138. }
  139. case discord.GuildStageVoice, discord.GuildVoice:
  140. if c.ParentID == 0 || c.ParentID == discord.NullChannelID {
  141. cn := tview.NewTreeNode("[::d]🔊" + c.Name + "[::-]").
  142. SetReference(c)
  143. gn.AddChild(cn)
  144. }
  145. }
  146. }
  147. }
  148. }
  149. func onGuildsTreeViewSelected(n *tview.TreeNode) {
  150. switch r := n.GetReference().(type) {
  151. case gateway.GuildCreateEvent:
  152. currentGuild = r
  153. n.SetExpanded(!n.IsExpanded())
  154. case discord.Channel:
  155. switch r.Type {
  156. case discord.GuildCategory:
  157. if len(n.GetChildren()) == 0 {
  158. for _, c := range currentGuild.Channels {
  159. switch c.Type {
  160. case discord.GuildText, discord.GuildNews:
  161. if c.ParentID == r.ID {
  162. cn := tview.NewTreeNode("[::d]#" + c.Name + "[::-]").
  163. SetReference(c)
  164. n.AddChild(cn)
  165. }
  166. case discord.GuildStageVoice, discord.GuildVoice:
  167. if c.ParentID == r.ID {
  168. cn := tview.NewTreeNode("[::d]🔊" + c.Name + "[::-]").
  169. SetReference(c)
  170. n.AddChild(cn)
  171. }
  172. }
  173. }
  174. } else {
  175. n.SetExpanded(!n.IsExpanded())
  176. }
  177. case discord.GuildText, discord.GuildNews:
  178. if len(n.GetChildren()) == 0 {
  179. currentChannel = r
  180. app.SetFocus(messageInputField)
  181. messagesTextView.Clear()
  182. title := "#" + r.Name
  183. if r.Topic != "" {
  184. title += " - " + r.Topic
  185. }
  186. messagesTextView.SetTitle(title)
  187. for _, t := range currentGuild.Threads {
  188. if t.ParentID == currentChannel.ID {
  189. cn := tview.NewTreeNode("[::d]🗨 " + t.Name + "[::-]").
  190. SetReference(t)
  191. n.AddChild(cn)
  192. }
  193. }
  194. go func() {
  195. msgs, _ := discordSession.Messages(r.ID, conf.GetMessagesLimit)
  196. for i := len(msgs) - 1; i >= 0; i-- {
  197. util.WriteMessage(messagesTextView, clientID, msgs[i])
  198. }
  199. }()
  200. } else {
  201. n.SetExpanded(!n.IsExpanded())
  202. }
  203. case discord.GuildNewsThread, discord.GuildPrivateThread, discord.GuildPublicThread:
  204. currentChannel = r
  205. app.SetFocus(messageInputField)
  206. messagesTextView.Clear()
  207. messagesTextView.SetTitle(r.Name)
  208. go func() {
  209. msgs, _ := discordSession.Messages(r.ID, conf.GetMessagesLimit)
  210. for i := len(msgs) - 1; i >= 0; i-- {
  211. util.WriteMessage(messagesTextView, clientID, msgs[i])
  212. }
  213. }()
  214. case discord.GuildStageVoice, discord.GuildVoice:
  215. messagesTextView.Clear()
  216. messagesTextView.SetTitle(r.Name)
  217. }
  218. }
  219. }
  220. func onLoginFormLoginButtonSelected() {
  221. email := loginForm.GetFormItem(0).(*tview.InputField).GetText()
  222. password := loginForm.GetFormItem(1).(*tview.InputField).GetText()
  223. if email == "" || password == "" {
  224. return
  225. }
  226. // Make a scratch HTTP client without a token
  227. client := api.NewClient("")
  228. // Try to login without TOTP
  229. l, err := client.Login(email, password)
  230. if err != nil {
  231. panic(err)
  232. }
  233. if l.Token != "" && !l.MFA {
  234. app.
  235. SetRoot(mainFlex, true).
  236. SetFocus(guildsTreeView)
  237. discordSession = newSession(l.Token)
  238. go util.SetItem(kr, "token", l.Token)
  239. } else if l.MFA {
  240. loginForm = ui.NewMfaLoginForm(func() {
  241. code := loginForm.GetFormItem(0).(*tview.InputField).GetText()
  242. if code == "" {
  243. return
  244. }
  245. l, err := client.TOTP(code, l.Ticket)
  246. if err != nil {
  247. panic(err)
  248. }
  249. app.
  250. SetRoot(mainFlex, true).
  251. SetFocus(guildsTreeView)
  252. discordSession = newSession(l.Token)
  253. go util.SetItem(kr, "token", l.Token)
  254. })
  255. app.SetRoot(loginForm, true)
  256. }
  257. }