discord.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "regexp"
  6. "sort"
  7. "strings"
  8. "github.com/ayntgl/discordgo"
  9. "github.com/ayntgl/discordo/util"
  10. "github.com/gen2brain/beeep"
  11. "github.com/rivo/tview"
  12. )
  13. var (
  14. boldRegex = regexp.MustCompile(`(?m)\*\*(.*?)\*\*`)
  15. italicRegex = regexp.MustCompile(`(?m)\*(.*?)\*`)
  16. underlineRegex = regexp.MustCompile(`(?m)__(.*?)__`)
  17. strikeThroughRegex = regexp.MustCompile(`(?m)~~(.*?)~~`)
  18. )
  19. func newSession() *discordgo.Session {
  20. s, err := discordgo.New()
  21. if err != nil {
  22. panic(err)
  23. }
  24. s.UserAgent = conf.UserAgent
  25. s.Identify.Compress = false
  26. s.Identify.Intents = 0
  27. s.Identify.LargeThreshold = 0
  28. s.Identify.Properties.Device = ""
  29. s.Identify.Properties.Browser = "Chrome"
  30. s.Identify.Properties.OS = "Linux"
  31. s.AddHandlerOnce(onSessionReady)
  32. s.AddHandler(onSessionMessageCreate)
  33. return s
  34. }
  35. func onSessionReady(_ *discordgo.Session, r *discordgo.Ready) {
  36. dmNode := tview.NewTreeNode("Direct Messages").
  37. Collapse()
  38. n := channelsTree.GetRoot()
  39. n.AddChild(dmNode)
  40. sort.Slice(r.Guilds, func(a, b int) bool {
  41. found := false
  42. for _, gID := range session.State.Settings.GuildPositions {
  43. if found {
  44. if gID == r.Guilds[b].ID {
  45. return true
  46. }
  47. } else {
  48. if gID == r.Guilds[a].ID {
  49. found = true
  50. }
  51. }
  52. }
  53. return false
  54. })
  55. for _, g := range r.Guilds {
  56. gn := tview.NewTreeNode(g.Name).
  57. SetReference(g.ID).
  58. Collapse()
  59. n.AddChild(gn)
  60. }
  61. channelsTree.SetCurrentNode(n)
  62. }
  63. func onSessionMessageCreate(_ *discordgo.Session, m *discordgo.MessageCreate) {
  64. c, err := session.State.Channel(m.ChannelID)
  65. if err != nil {
  66. return
  67. }
  68. if selectedChannel == nil || selectedChannel.ID != m.ChannelID {
  69. if conf.Notifications {
  70. for _, u := range m.Mentions {
  71. if u.ID == session.State.User.ID {
  72. g, err := session.State.Guild(m.GuildID)
  73. if err != nil {
  74. return
  75. }
  76. go beeep.Alert(fmt.Sprintf("%s (#%s)", g.Name, c.Name), m.ContentWithMentionsReplaced(), "")
  77. return
  78. }
  79. }
  80. }
  81. cn := util.GetTreeNodeByReference(channelsTree, c.ID)
  82. if cn == nil {
  83. return
  84. }
  85. cn.SetText("[::b]" + util.ChannelToString(c) + "[::-]")
  86. app.Draw()
  87. } else {
  88. selectedChannel.Messages = append(selectedChannel.Messages, m.Message)
  89. messagesView.Write(buildMessage(m.Message))
  90. // Scroll to the end of the text after the message has been written to the TextView.
  91. messagesView.ScrollToEnd()
  92. }
  93. }
  94. type loginResponse struct {
  95. MFA bool `json:"mfa"`
  96. SMS bool `json:"sms"`
  97. Ticket string `json:"ticket"`
  98. Token string `json:"token"`
  99. }
  100. func login(email, password string) (*loginResponse, error) {
  101. data := struct {
  102. Email string `json:"email"`
  103. Password string `json:"password"`
  104. }{email, password}
  105. resp, err := session.RequestWithBucketID(
  106. "POST",
  107. discordgo.EndpointLogin,
  108. data,
  109. discordgo.EndpointLogin,
  110. )
  111. if err != nil {
  112. return nil, err
  113. }
  114. var lr loginResponse
  115. err = json.Unmarshal(resp, &lr)
  116. if err != nil {
  117. return nil, err
  118. }
  119. return &lr, nil
  120. }
  121. func totp(code, ticket string) (*loginResponse, error) {
  122. data := struct {
  123. Code string `json:"code"`
  124. Ticket string `json:"ticket"`
  125. }{code, ticket}
  126. e := discordgo.EndpointAuth + "mfa/totp"
  127. resp, err := session.RequestWithBucketID("POST", e, data, e)
  128. if err != nil {
  129. return nil, err
  130. }
  131. var lr loginResponse
  132. err = json.Unmarshal(resp, &lr)
  133. if err != nil {
  134. return nil, err
  135. }
  136. return &lr, nil
  137. }
  138. func buildMessage(m *discordgo.Message) []byte {
  139. var b strings.Builder
  140. switch m.Type {
  141. case discordgo.MessageTypeDefault, discordgo.MessageTypeReply:
  142. // Define a new region and assign message ID as the region ID.
  143. // Learn more:
  144. // https://pkg.go.dev/github.com/rivo/tview#hdr-Regions_and_Highlights
  145. b.WriteString("[\"")
  146. b.WriteString(m.ID)
  147. b.WriteString("\"]")
  148. // Build the message associated with crosspost, channel follow add, pin, or a reply.
  149. buildReferencedMessage(&b, m.ReferencedMessage)
  150. // Build the author of this message.
  151. buildAuthor(&b, m.Author)
  152. // Build the contents of the message.
  153. buildContent(&b, m)
  154. if m.EditedTimestamp != "" {
  155. b.WriteString(" [::d](edited)[::-]")
  156. }
  157. // Build the embeds associated with the message.
  158. buildEmbeds(&b, m.Embeds)
  159. // Build the message attachments (attached files to the message).
  160. buildAttachments(&b, m.Attachments)
  161. // Tags with no region ID ([""]) do not start new regions. They can
  162. // therefore be used to mark the end of a region.
  163. b.WriteString("[\"\"]")
  164. b.WriteByte('\n')
  165. case discordgo.MessageTypeGuildMemberJoin:
  166. b.WriteString("[#5865F2]")
  167. b.WriteString(m.Author.Username)
  168. b.WriteString("[-] joined the server.")
  169. b.WriteByte('\n')
  170. case discordgo.MessageTypeCall:
  171. b.WriteString("[#5865F2]")
  172. b.WriteString(m.Author.Username)
  173. b.WriteString("[-] started a call.")
  174. b.WriteByte('\n')
  175. case discordgo.MessageTypeChannelPinnedMessage:
  176. b.WriteString("[#5865F2]")
  177. b.WriteString(m.Author.Username)
  178. b.WriteString("[-] pinned a message.")
  179. b.WriteByte('\n')
  180. }
  181. if str := b.String(); str != "" {
  182. b := make([]byte, len(str)+1)
  183. copy(b, str)
  184. return b
  185. }
  186. return nil
  187. }
  188. func buildReferencedMessage(b *strings.Builder, rm *discordgo.Message) {
  189. if rm != nil {
  190. b.WriteString(" ╭ ")
  191. b.WriteString("[::d]")
  192. buildAuthor(b, rm.Author)
  193. if rm.Content != "" {
  194. rm.Content = buildMentions(rm.Content, rm.Mentions)
  195. b.WriteString(parseMarkdown(rm.Content))
  196. }
  197. b.WriteString("[::-]")
  198. b.WriteByte('\n')
  199. }
  200. }
  201. func buildContent(b *strings.Builder, m *discordgo.Message) {
  202. if m.Content != "" {
  203. m.Content = buildMentions(m.Content, m.Mentions)
  204. b.WriteString(parseMarkdown(m.Content))
  205. }
  206. }
  207. func buildEmbeds(b *strings.Builder, es []*discordgo.MessageEmbed) {
  208. for _, e := range es {
  209. if e.Type != discordgo.EmbedTypeRich {
  210. continue
  211. }
  212. var embedBuilder strings.Builder
  213. var hasHeading bool
  214. prefix := fmt.Sprintf("[#%06X]▐[-] ", e.Color)
  215. b.WriteByte('\n')
  216. embedBuilder.WriteString(prefix)
  217. if e.Author != nil {
  218. hasHeading = true
  219. embedBuilder.WriteString("[::u]")
  220. embedBuilder.WriteString(e.Author.Name)
  221. embedBuilder.WriteString("[::-]")
  222. }
  223. if e.Title != "" {
  224. hasHeading = true
  225. embedBuilder.WriteString("[::b]")
  226. embedBuilder.WriteString(e.Title)
  227. embedBuilder.WriteString("[::-]")
  228. }
  229. if e.Description != "" {
  230. if hasHeading {
  231. embedBuilder.WriteString("\n\n")
  232. }
  233. embedBuilder.WriteString(parseMarkdown(e.Description))
  234. }
  235. if len(e.Fields) != 0 {
  236. if hasHeading || e.Description != "" {
  237. embedBuilder.WriteString("\n\n")
  238. }
  239. for i, ef := range e.Fields {
  240. embedBuilder.WriteString("[::b]")
  241. embedBuilder.WriteString(ef.Name)
  242. embedBuilder.WriteString("[::-]")
  243. embedBuilder.WriteByte('\n')
  244. embedBuilder.WriteString(parseMarkdown(ef.Value))
  245. if i != len(e.Fields)-1 {
  246. embedBuilder.WriteString("\n\n")
  247. }
  248. }
  249. }
  250. if e.Footer != nil {
  251. if hasHeading {
  252. embedBuilder.WriteString("\n\n")
  253. }
  254. embedBuilder.WriteString(e.Footer.Text)
  255. }
  256. b.WriteString(strings.Replace(embedBuilder.String(), "\n", "\n"+prefix, -1))
  257. }
  258. }
  259. func buildAttachments(b *strings.Builder, as []*discordgo.MessageAttachment) {
  260. for _, a := range as {
  261. b.WriteByte('\n')
  262. b.WriteByte('[')
  263. b.WriteString(a.Filename)
  264. b.WriteString("]: ")
  265. b.WriteString(a.URL)
  266. }
  267. }
  268. func buildMentions(content string, mentions []*discordgo.User) string {
  269. for _, mUser := range mentions {
  270. var color string
  271. if mUser.ID == session.State.User.ID {
  272. color = "[:#5865F2]"
  273. } else {
  274. color = "[#EB459E]"
  275. }
  276. content = strings.NewReplacer(
  277. // <@USER_ID>
  278. "<@"+mUser.ID+">",
  279. color+"@"+mUser.Username+"[-:-]",
  280. // <@!USER_ID>
  281. "<@!"+mUser.ID+">",
  282. color+"@"+mUser.Username+"[-:-]",
  283. ).Replace(content)
  284. }
  285. return content
  286. }
  287. func buildAuthor(b *strings.Builder, u *discordgo.User) {
  288. if u.ID == session.State.User.ID {
  289. b.WriteString("[#57F287]")
  290. } else {
  291. b.WriteString("[#ED4245]")
  292. }
  293. b.WriteString(u.Username)
  294. b.WriteString("[-] ")
  295. // If the message author is a bot account, render the message with bot label
  296. // for distinction.
  297. if u.Bot {
  298. b.WriteString("[#EB459E]BOT[-] ")
  299. }
  300. }
  301. func parseMarkdown(md string) string {
  302. var res string
  303. res = boldRegex.ReplaceAllString(md, "[::b]$1[::-]")
  304. res = italicRegex.ReplaceAllString(res, "[::i]$1[::-]")
  305. res = underlineRegex.ReplaceAllString(res, "[::u]$1[::-]")
  306. res = strikeThroughRegex.ReplaceAllString(res, "[::s]$1[::-]")
  307. return res
  308. }