discordo.go 7.4 KB

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