discordo.go 7.4 KB

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