discordo/discordo.go
2021-08-19 15:45:08 +04:00

303 lines
7.2 KiB
Go

package main
import (
"os"
"sort"
"strings"
"github.com/atotto/clipboard"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/gateway"
"github.com/diamondburned/arikawa/v3/state"
"github.com/gdamore/tcell/v2"
"github.com/rigormorrtiss/discordo/ui"
"github.com/rigormorrtiss/discordo/util"
"github.com/rivo/tview"
)
var (
app *tview.Application
loginForm *tview.Form
guildsTreeView *tview.TreeView
channelsTreeView *tview.TreeView
messagesTextView *tview.TextView
messageInputField *tview.InputField
mainFlex *tview.Flex
conf *util.Config
discordState *state.State
channel *discord.Channel
)
func main() {
conf = util.NewConfig()
tview.Styles.PrimitiveBackgroundColor = tcell.GetColor(conf.Theme.Background)
if !conf.Theme.Borders {
tview.Borders = struct {
Horizontal rune
Vertical rune
TopLeft rune
TopRight rune
BottomLeft rune
BottomRight rune
LeftT rune
RightT rune
TopT rune
BottomT rune
Cross rune
HorizontalFocus rune
VerticalFocus rune
TopLeftFocus rune
TopRightFocus rune
BottomLeftFocus rune
BottomRightFocus rune
}{}
}
app = ui.NewApp(onAppInputCapture)
guildsTreeView = ui.NewGuildsTreeView(onGuildsTreeViewSelected)
channelsTreeView = ui.NewChannelsTreeView(onChannelsTreeViewSelected)
messagesTextView = ui.NewMessagesTextView(app)
messageInputField = ui.NewMessageInputField(onMessageInputFieldInputCapture, conf.Theme)
mainFlex = ui.NewMainFlex(guildsTreeView, channelsTreeView, messagesTextView, messageInputField)
api.UserAgent = "" +
"Mozilla/5.0 (X11; Linux x86_64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/92.0.4515.131 Safari/537.36"
gateway.DefaultIdentity.Browser = "Chrome"
gateway.DefaultIdentity.OS = "Linux"
gateway.DefaultIdentity.Device = ""
token := os.Getenv("DISCORDO_TOKEN")
if t := util.GetPassword("token"); t != "" {
token = t
}
if token != "" {
app.
SetRoot(mainFlex, true).
SetFocus(guildsTreeView)
discordState = newState(token)
} else {
loginForm = ui.NewLoginForm(onLoginFormLoginButtonSelected)
app.SetRoot(loginForm, true)
}
if err := app.Run(); err != nil {
panic(err)
}
}
func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey {
switch e.Name() {
case "Alt+Rune[1]":
app.SetFocus(guildsTreeView)
case "Alt+Rune[2]":
app.SetFocus(channelsTreeView)
case "Alt+Rune[3]":
app.SetFocus(messagesTextView)
case "Alt+Rune[4]":
app.SetFocus(messageInputField)
}
return e
}
func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
switch e.Key() {
case tcell.KeyEnter:
t := strings.TrimSpace(messageInputField.GetText())
if t == "" {
return nil
}
discordState.SendMessage(channel.ID, t)
messageInputField.SetText("")
case tcell.KeyCtrlV:
text, _ := clipboard.ReadAll()
text = messageInputField.GetText() + text
messageInputField.SetText(text)
}
return e
}
func newState(token string) (s *state.State) {
var err error
s, err = state.New(token)
if err != nil {
panic(err)
}
s.AddHandler(onSessionReady)
s.AddHandler(onSessionMessageCreate)
if err = s.Open(s.Context()); err != nil {
panic(err)
}
return
}
func onSessionReady(r *gateway.ReadyEvent) {
rootN := guildsTreeView.GetRoot()
for _, g := range r.Guilds {
gn := tview.NewTreeNode(g.Name).
SetReference(g.ID)
rootN.AddChild(gn)
}
guildsTreeView.SetCurrentNode(rootN)
}
func onSessionMessageCreate(m *gateway.MessageCreateEvent) {
if channel != nil && channel.ID == m.ChannelID {
me, _ := discordState.Cabinet.Me()
util.WriteMessage(messagesTextView, me.ID, m.Message)
}
}
func onGuildsTreeViewSelected(gn *tview.TreeNode) {
app.SetFocus(channelsTreeView)
messagesTextView.SetTitle("")
messagesTextView.Clear()
gID := gn.GetReference().(discord.GuildID)
cs, _ := discordState.Cabinet.Channels(gID)
sort.Slice(cs, func(i, j int) bool {
return cs[i].Position < cs[j].Position
})
rootN := channelsTreeView.GetRoot()
rootN.ClearChildren()
// Top-level channels
for _, c := range cs {
if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (c.ParentID == 0 || c.ParentID == discord.NullChannelID) {
cn := ui.NewTextChannelTreeNode(c)
rootN.AddChild(cn)
continue
}
}
// Category channels
CategoryLoop:
for _, c := range cs {
if c.Type == discord.GuildCategory {
for _, child := range cs {
if child.ParentID == c.ID {
cn := tview.NewTreeNode(c.Name).
SetReference(c.ID)
rootN.AddChild(cn)
continue CategoryLoop
}
}
cn := tview.NewTreeNode(c.Name).
SetReference(c.ID)
rootN.AddChild(cn)
}
}
// Second-level channels
for _, c := range cs {
if (c.Type == discord.GuildText || c.Type == discord.GuildNews) && (c.ParentID != 0 && c.ParentID != discord.NullChannelID) {
if pn := ui.GetTreeNodeByReference(c.ParentID, channelsTreeView); pn != nil {
cn := ui.NewTextChannelTreeNode(c)
pn.AddChild(cn)
}
}
}
channelsTreeView.SetCurrentNode(rootN)
}
func onChannelsTreeViewSelected(n *tview.TreeNode) {
cID := n.GetReference().(discord.ChannelID)
c, _ := discordState.Cabinet.Channel(cID)
switch c.Type {
case discord.GuildCategory:
n.SetExpanded(!n.IsExpanded())
case discord.GuildText, discord.GuildNews:
if len(n.GetChildren()) == 0 {
channel = c
app.SetFocus(messageInputField)
messagesTextView.Clear()
title := "#" + c.Name
if c.Topic != "" {
title += " - " + c.Topic
}
messagesTextView.SetTitle(title)
go writeMessages(c.ID)
} else {
n.SetExpanded(!n.IsExpanded())
}
case discord.GuildNewsThread, discord.GuildPrivateThread, discord.GuildPublicThread:
channel = c
app.SetFocus(messageInputField)
messagesTextView.Clear()
messagesTextView.SetTitle(c.Name)
go writeMessages(c.ID)
case discord.GuildStageVoice, discord.GuildVoice:
messagesTextView.Clear()
messagesTextView.SetTitle(c.Name)
}
}
func writeMessages(cID discord.ChannelID) {
msgs, _ := discordState.Messages(cID, conf.GetMessagesLimit)
me, _ := discordState.Cabinet.Me()
for i := len(msgs) - 1; i >= 0; i-- {
util.WriteMessage(messagesTextView, me.ID, msgs[i])
}
}
func onLoginFormLoginButtonSelected() {
email := loginForm.GetFormItem(0).(*tview.InputField).GetText()
password := loginForm.GetFormItem(1).(*tview.InputField).GetText()
if email == "" || password == "" {
return
}
// Make a scratch HTTP client without a token
client := api.NewClient("")
// Try to login without TOTP
l, err := client.Login(email, password)
if err != nil {
panic(err)
}
if l.Token != "" && !l.MFA {
app.
SetRoot(mainFlex, true).
SetFocus(guildsTreeView)
discordState = newState(l.Token)
go util.SetPassword("token", l.Token)
} else if l.MFA {
loginForm = ui.NewMfaLoginForm(func() {
code := loginForm.GetFormItem(0).(*tview.InputField).GetText()
if code == "" {
return
}
l, err := client.TOTP(code, l.Ticket)
if err != nil {
panic(err)
}
app.
SetRoot(mainFlex, true).
SetFocus(guildsTreeView)
discordState = newState(l.Token)
go util.SetPassword("token", l.Token)
})
app.SetRoot(loginForm, true)
}
}