discordo.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  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. guildsList *tview.List
  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. guild gateway.GuildCreateEvent
  27. channel *discord.Channel
  28. )
  29. func main() {
  30. conf = util.NewConfig()
  31. tview.Styles.PrimitiveBackgroundColor = tcell.GetColor(conf.Theme.Background)
  32. if !conf.Theme.Borders {
  33. tview.Borders = struct {
  34. Horizontal rune
  35. Vertical rune
  36. TopLeft rune
  37. TopRight rune
  38. BottomLeft rune
  39. BottomRight rune
  40. LeftT rune
  41. RightT rune
  42. TopT rune
  43. BottomT rune
  44. Cross rune
  45. HorizontalFocus rune
  46. VerticalFocus rune
  47. TopLeftFocus rune
  48. TopRightFocus rune
  49. BottomLeftFocus rune
  50. BottomRightFocus rune
  51. }{}
  52. }
  53. app = ui.NewApp(onAppInputCapture)
  54. guildsList = ui.NewGuildsList(onGuildsListSelected, conf.Theme)
  55. channelsTreeView = ui.NewChannelsTreeView(onChannelsTreeViewSelected)
  56. messagesTextView = ui.NewMessagesTextView(app)
  57. messageInputField = ui.NewMessageInputField(onMessageInputFieldInputCapture, conf.Theme)
  58. mainFlex = ui.NewMainFlex(guildsList, channelsTreeView, messagesTextView, messageInputField)
  59. token := os.Getenv("DISCORDO_TOKEN")
  60. if t := util.GetPassword("token"); t != "" {
  61. token = t
  62. }
  63. if token != "" {
  64. app.
  65. SetRoot(mainFlex, true).
  66. SetFocus(guildsList)
  67. discordState = newState(token)
  68. } else {
  69. loginForm = ui.NewLoginForm(onLoginFormLoginButtonSelected)
  70. app.SetRoot(loginForm, true)
  71. }
  72. if err := app.Run(); err != nil {
  73. panic(err)
  74. }
  75. }
  76. func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey {
  77. switch e.Name() {
  78. case "Alt+Rune[1]":
  79. app.SetFocus(guildsList)
  80. case "Alt+Rune[2]":
  81. app.SetFocus(channelsTreeView)
  82. case "Alt+Rune[3]":
  83. app.SetFocus(messagesTextView)
  84. case "Alt+Rune[4]":
  85. app.SetFocus(messageInputField)
  86. }
  87. return e
  88. }
  89. func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
  90. switch e.Key() {
  91. case tcell.KeyEnter:
  92. t := strings.TrimSpace(messageInputField.GetText())
  93. if t == "" {
  94. return nil
  95. }
  96. discordState.SendMessage(channel.ID, t)
  97. messageInputField.SetText("")
  98. case tcell.KeyCtrlV:
  99. text, _ := clipboard.ReadAll()
  100. text = messageInputField.GetText() + text
  101. messageInputField.SetText(text)
  102. }
  103. return e
  104. }
  105. func newState(token string) (s *state.State) {
  106. api.UserAgent = "" +
  107. "Mozilla/5.0 (X11; Linux x86_64) " +
  108. "AppleWebKit/537.36 (KHTML, like Gecko) " +
  109. "Chrome/92.0.4515.131 Safari/537.36"
  110. gateway.DefaultIdentity.Browser = "Chrome"
  111. gateway.DefaultIdentity.OS = "Linux"
  112. gateway.DefaultIdentity.Device = ""
  113. var err error
  114. s, err = state.New(token)
  115. if err != nil {
  116. panic(err)
  117. }
  118. s.AddHandler(onSessionReady)
  119. s.AddHandler(onSessionMessageCreate)
  120. if err = s.Open(s.Context()); err != nil {
  121. panic(err)
  122. }
  123. return
  124. }
  125. func onSessionReady(r *gateway.ReadyEvent) {
  126. for _, g := range r.Guilds {
  127. guildsList.AddItem(g.Name, "", 0, nil)
  128. }
  129. }
  130. func onSessionMessageCreate(m *gateway.MessageCreateEvent) {
  131. if channel != nil && channel.ID == m.ChannelID {
  132. me, _ := discordState.Cabinet.Me()
  133. util.WriteMessage(messagesTextView, me.ID, m.Message)
  134. }
  135. }
  136. func onGuildsListSelected(i int, _ string, _ string, _ rune) {
  137. app.SetFocus(channelsTreeView)
  138. messagesTextView.SetTitle("")
  139. messagesTextView.Clear()
  140. guild = discordState.Ready().Guilds[i]
  141. cs := guild.Channels
  142. sort.Slice(cs, func(i, j int) bool {
  143. return guild.Channels[i].Position < guild.Channels[j].Position
  144. })
  145. n := channelsTreeView.GetRoot()
  146. n.ClearChildren()
  147. // Top-level channels
  148. for _, c := range cs {
  149. if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (c.ParentID == 0 || c.ParentID == discord.NullChannelID) {
  150. cn := util.NewTextChannelTreeNode(c)
  151. n.AddChild(cn)
  152. continue
  153. }
  154. }
  155. // Category channels
  156. CategoryLoop:
  157. for _, c := range cs {
  158. if c.Type == discord.GuildCategory {
  159. for _, child := range cs {
  160. if child.ParentID == c.ID {
  161. cn := tview.NewTreeNode(c.Name).
  162. SetReference(c.ID)
  163. n.AddChild(cn)
  164. continue CategoryLoop
  165. }
  166. }
  167. }
  168. }
  169. // Second-level channels
  170. for _, c := range cs {
  171. if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (c.ParentID != 0 && c.ParentID != discord.NullChannelID) {
  172. if pn := util.GetTreeNodeByReference(c.ParentID, channelsTreeView); pn != nil {
  173. cn := util.NewTextChannelTreeNode(c)
  174. pn.AddChild(cn)
  175. }
  176. }
  177. }
  178. }
  179. func onChannelsTreeViewSelected(n *tview.TreeNode) {
  180. cID := n.GetReference().(discord.ChannelID)
  181. c, _ := discordState.Cabinet.Channel(cID)
  182. switch c.Type {
  183. case discord.GuildCategory:
  184. n.SetExpanded(!n.IsExpanded())
  185. case discord.GuildText, discord.GuildNews:
  186. if len(n.GetChildren()) == 0 {
  187. channel = c
  188. app.SetFocus(messageInputField)
  189. messagesTextView.Clear()
  190. title := "#" + c.Name
  191. if c.Topic != "" {
  192. title += " - " + c.Topic
  193. }
  194. messagesTextView.SetTitle(title)
  195. for _, t := range guild.Threads {
  196. if t.ParentID == channel.ID {
  197. cn := tview.NewTreeNode("[::d]🗨 " + t.Name + "[::-]").
  198. SetReference(t)
  199. n.AddChild(cn)
  200. }
  201. }
  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. for i := len(msgs) - 1; i >= 0; i-- {
  220. me, _ := discordState.Cabinet.Me()
  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(guildsList, true).
  240. SetFocus(channelsTreeView)
  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(guildsList)
  256. discordState = newState(l.Token)
  257. go util.SetPassword("token", l.Token)
  258. })
  259. app.SetRoot(loginForm, true)
  260. }
  261. }