discordo.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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. ui.CreateTopLevelTreeNodes(rootN, cs)
  152. // Category channels
  153. CategoryLoop:
  154. for _, c := range cs {
  155. if c.Type == discord.GuildCategory {
  156. for _, child := range cs {
  157. if child.ParentID == c.ID {
  158. cn := tview.NewTreeNode(c.Name).
  159. SetReference(c.ID)
  160. rootN.AddChild(cn)
  161. continue CategoryLoop
  162. }
  163. }
  164. cn := tview.NewTreeNode(c.Name).
  165. SetReference(c.ID)
  166. rootN.AddChild(cn)
  167. }
  168. }
  169. // Second-level channels
  170. ui.CreateSecondLevelTreeNodes(channelsTreeView, rootN, cs)
  171. channelsTreeView.SetCurrentNode(rootN)
  172. }
  173. func onChannelsTreeViewSelected(n *tview.TreeNode) {
  174. cID := n.GetReference().(discord.ChannelID)
  175. c, _ := discordState.Cabinet.Channel(cID)
  176. switch c.Type {
  177. case discord.GuildCategory:
  178. n.SetExpanded(!n.IsExpanded())
  179. case discord.GuildText, discord.GuildNews:
  180. if len(n.GetChildren()) == 0 {
  181. channel = c
  182. app.SetFocus(messageInputField)
  183. messagesTextView.Clear()
  184. title := "#" + c.Name
  185. if c.Topic != "" {
  186. title += " - " + c.Topic
  187. }
  188. messagesTextView.SetTitle(title)
  189. go writeMessages(c.ID)
  190. } else {
  191. n.SetExpanded(!n.IsExpanded())
  192. }
  193. case discord.GuildNewsThread, discord.GuildPrivateThread, discord.GuildPublicThread:
  194. channel = c
  195. app.SetFocus(messageInputField)
  196. messagesTextView.Clear()
  197. messagesTextView.SetTitle(c.Name)
  198. go writeMessages(c.ID)
  199. case discord.GuildStageVoice, discord.GuildVoice:
  200. messagesTextView.Clear()
  201. messagesTextView.SetTitle(c.Name)
  202. }
  203. }
  204. func writeMessages(cID discord.ChannelID) {
  205. msgs, _ := discordState.Messages(cID, conf.GetMessagesLimit)
  206. me, _ := discordState.Cabinet.Me()
  207. for i := len(msgs) - 1; i >= 0; i-- {
  208. util.WriteMessage(messagesTextView, me.ID, msgs[i])
  209. }
  210. }
  211. func onLoginFormLoginButtonSelected() {
  212. email := loginForm.GetFormItem(0).(*tview.InputField).GetText()
  213. password := loginForm.GetFormItem(1).(*tview.InputField).GetText()
  214. if email == "" || password == "" {
  215. return
  216. }
  217. // Make a scratch HTTP client without a token
  218. client := api.NewClient("")
  219. // Try to login without TOTP
  220. l, err := client.Login(email, password)
  221. if err != nil {
  222. panic(err)
  223. }
  224. if l.Token != "" && !l.MFA {
  225. app.
  226. SetRoot(mainFlex, true).
  227. SetFocus(guildsTreeView)
  228. discordState = newState(l.Token)
  229. go util.SetPassword("token", l.Token)
  230. } else if l.MFA {
  231. loginForm = ui.NewMfaLoginForm(func() {
  232. code := loginForm.GetFormItem(0).(*tview.InputField).GetText()
  233. if code == "" {
  234. return
  235. }
  236. l, err := client.TOTP(code, l.Ticket)
  237. if err != nil {
  238. panic(err)
  239. }
  240. app.
  241. SetRoot(mainFlex, true).
  242. SetFocus(guildsTreeView)
  243. discordState = newState(l.Token)
  244. go util.SetPassword("token", l.Token)
  245. })
  246. app.SetRoot(loginForm, true)
  247. }
  248. }