discordo.go 7.5 KB

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