discordo.go 7.2 KB

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