discordo.go 6.8 KB

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