Switch to a flat project structure (#48)
This commit is contained in:
parent
8c27d2b138
commit
f945fbf4f8
9 changed files with 676 additions and 745 deletions
|
|
@ -1,4 +1,4 @@
|
|||
package util
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -7,9 +7,7 @@ import (
|
|||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// Config consists of fields, such as theme, mouse, so on, that may be
|
||||
// customized by the user.
|
||||
type Config struct {
|
||||
type config struct {
|
||||
Token string `json:"token"`
|
||||
Mouse bool `json:"mouse"`
|
||||
Notifications bool `json:"notifications"`
|
||||
|
|
@ -18,8 +16,7 @@ type Config struct {
|
|||
Theme tview.Theme `json:"theme"`
|
||||
}
|
||||
|
||||
// LoadConfig creates (if the configuration file does not exist) and reads the configuration file and returns a new config.
|
||||
func LoadConfig() *Config {
|
||||
func loadConfig() *config {
|
||||
u, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
@ -32,7 +29,7 @@ func LoadConfig() *Config {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
c := Config{
|
||||
c := config{
|
||||
Mouse: true,
|
||||
Notifications: true,
|
||||
UserAgent: "" +
|
||||
|
|
@ -60,7 +57,7 @@ func LoadConfig() *Config {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
var c Config
|
||||
var c config
|
||||
if err = json.Unmarshal(d, &c); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
144
discord.go
Normal file
144
discord.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/gen2brain/beeep"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func newSession() *discordgo.Session {
|
||||
s, err := discordgo.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.UserAgent = conf.UserAgent
|
||||
s.Identify.Compress = false
|
||||
s.Identify.Intents = 0
|
||||
s.Identify.LargeThreshold = 0
|
||||
s.Identify.Properties.Device = ""
|
||||
s.Identify.Properties.Browser = "Chrome"
|
||||
s.Identify.Properties.OS = "Linux"
|
||||
|
||||
s.AddHandlerOnce(onSessionReady)
|
||||
s.AddHandler(onSessionMessageCreate)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func onSessionReady(_ *discordgo.Session, r *discordgo.Ready) {
|
||||
sort.Slice(r.Guilds, func(a, b int) bool {
|
||||
found := false
|
||||
for _, gID := range r.Settings.GuildPositions {
|
||||
if found {
|
||||
if gID == r.Guilds[b].ID {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if gID == r.Guilds[a].ID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
n := guildsTreeView.GetRoot()
|
||||
for _, g := range r.Guilds {
|
||||
gn := tview.NewTreeNode(g.Name).
|
||||
SetReference(g.ID)
|
||||
n.AddChild(gn)
|
||||
}
|
||||
|
||||
guildsTreeView.SetCurrentNode(n)
|
||||
}
|
||||
|
||||
func onSessionMessageCreate(_ *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
if selectedChannel == nil {
|
||||
selectedChannel = &discordgo.Channel{ID: ""}
|
||||
}
|
||||
|
||||
if selectedChannel.ID != m.ChannelID {
|
||||
if conf.Notifications {
|
||||
for _, u := range m.Mentions {
|
||||
if u.ID == session.State.User.ID {
|
||||
g, err := session.State.Guild(m.GuildID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c, err := session.State.Channel(m.ChannelID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go beeep.Alert(fmt.Sprintf("%s (#%s)", g.Name, c.Name), m.ContentWithMentionsReplaced(), "")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
selectedChannel.Messages = append(selectedChannel.Messages, m.Message)
|
||||
renderMessage(m.Message)
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
MFA bool `json:"mfa"`
|
||||
SMS bool `json:"sms"`
|
||||
Ticket string `json:"ticket"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func login(
|
||||
s *discordgo.Session,
|
||||
email, password string,
|
||||
) (*loginResponse, error) {
|
||||
data := struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}{email, password}
|
||||
resp, err := s.RequestWithBucketID(
|
||||
"POST",
|
||||
discordgo.EndpointLogin,
|
||||
data,
|
||||
discordgo.EndpointLogin,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lr loginResponse
|
||||
err = json.Unmarshal(resp, &lr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &lr, nil
|
||||
}
|
||||
|
||||
func totp(s *discordgo.Session, code, ticket string) (*loginResponse, error) {
|
||||
data := struct {
|
||||
Code string `json:"code"`
|
||||
Ticket string `json:"ticket"`
|
||||
}{code, ticket}
|
||||
e := discordgo.EndpointAuth + "mfa/totp"
|
||||
resp, err := s.RequestWithBucketID("POST", e, data, e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lr loginResponse
|
||||
err = json.Unmarshal(resp, &lr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &lr, nil
|
||||
}
|
||||
455
discordo.go
455
discordo.go
|
|
@ -1,455 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gen2brain/beeep"
|
||||
"github.com/rigormorrtiss/discordo/ui"
|
||||
"github.com/rigormorrtiss/discordo/util"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
var (
|
||||
app *tview.Application
|
||||
loginWidget *tview.Form
|
||||
guildsWidget *tview.TreeView
|
||||
messagesWidget *tview.TextView
|
||||
inputWidget *tview.InputField
|
||||
mainFlex *tview.Flex
|
||||
|
||||
config *util.Config
|
||||
session *discordgo.Session
|
||||
selectedChannel *discordgo.Channel
|
||||
selectedMessage *discordgo.Message
|
||||
)
|
||||
|
||||
func main() {
|
||||
config = util.LoadConfig()
|
||||
tview.Styles = config.Theme
|
||||
|
||||
app = tview.NewApplication()
|
||||
app.EnableMouse(config.Mouse)
|
||||
app.SetInputCapture(onAppInputCapture)
|
||||
|
||||
guildsWidget = ui.NewGuildsWidget()
|
||||
guildsWidget.SetSelectedFunc(onGuildsWidgetSelected)
|
||||
|
||||
messagesWidget = ui.NewMessagesWidget(app)
|
||||
messagesWidget.SetInputCapture(onMessagesWidgetInputCapture)
|
||||
|
||||
inputWidget = ui.NewInputWidget()
|
||||
inputWidget.SetInputCapture(onInputWidgetInputCapture)
|
||||
|
||||
mainFlex = ui.NewMainFlex(
|
||||
guildsWidget,
|
||||
messagesWidget,
|
||||
inputWidget,
|
||||
)
|
||||
|
||||
token := config.Token
|
||||
if t, _ := keyring.Get("discordo", "token"); t != "" {
|
||||
token = t
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
app.
|
||||
SetRoot(mainFlex, true).
|
||||
SetFocus(guildsWidget)
|
||||
|
||||
session = newSession()
|
||||
session.Token = token
|
||||
session.Identify.Token = token
|
||||
if err := session.Open(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
loginWidget = ui.NewLoginWidget(onLoginFormLoginButtonSelected, false)
|
||||
app.SetRoot(loginWidget, true)
|
||||
}
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
if e.Modifiers() == tcell.ModAlt {
|
||||
switch e.Rune() {
|
||||
case '1':
|
||||
app.SetFocus(guildsWidget)
|
||||
case '2':
|
||||
app.SetFocus(messagesWidget)
|
||||
case '3':
|
||||
app.SetFocus(inputWidget)
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func findByMessageID(ms []*discordgo.Message, mID string) (int, *discordgo.Message) {
|
||||
for i, m := range ms {
|
||||
if mID == m.ID {
|
||||
return i, m
|
||||
}
|
||||
}
|
||||
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func onMessagesWidgetInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
if selectedChannel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case e.Key() == tcell.KeyUp || e.Rune() == 'k': // Up
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hs := messagesWidget.GetHighlights()
|
||||
if len(hs) == 0 {
|
||||
messagesWidget.
|
||||
Highlight(ms[len(ms)-1].ID).
|
||||
ScrollToHighlight()
|
||||
} else {
|
||||
idx, _ := findByMessageID(ms, hs[0])
|
||||
if idx == -1 || idx == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
messagesWidget.
|
||||
Highlight(ms[idx-1].ID).
|
||||
ScrollToHighlight()
|
||||
}
|
||||
|
||||
return nil
|
||||
case e.Key() == tcell.KeyDown || e.Rune() == 'j': // Down
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hs := messagesWidget.GetHighlights()
|
||||
if len(hs) == 0 {
|
||||
messagesWidget.
|
||||
Highlight(ms[len(ms)-1].ID).
|
||||
ScrollToHighlight()
|
||||
} else {
|
||||
idx, _ := findByMessageID(ms, hs[0])
|
||||
if idx == -1 || idx == len(ms)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
messagesWidget.
|
||||
Highlight(ms[idx+1].ID).
|
||||
ScrollToHighlight()
|
||||
}
|
||||
|
||||
return nil
|
||||
case e.Key() == tcell.KeyHome || e.Rune() == 'g': // Top
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
messagesWidget.
|
||||
Highlight(ms[0].ID).
|
||||
ScrollToHighlight()
|
||||
case e.Key() == tcell.KeyEnd || e.Rune() == 'G': // Bottom
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
messagesWidget.
|
||||
Highlight(ms[len(ms)-1].ID).
|
||||
ScrollToHighlight()
|
||||
case e.Rune() == 'r': // Inline reply
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hs := messagesWidget.GetHighlights()
|
||||
if len(hs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, selectedMessage = findByMessageID(ms, hs[0])
|
||||
inputWidget.SetTitle(
|
||||
"Replying to " + selectedMessage.Author.Username,
|
||||
)
|
||||
app.SetFocus(inputWidget)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func onInputWidgetInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
// If the "Alt" modifier key is pressed, do not handle the event.
|
||||
if e.Modifiers() == tcell.ModAlt {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch e.Key() {
|
||||
case tcell.KeyEnter:
|
||||
if selectedChannel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
t := strings.TrimSpace(inputWidget.GetText())
|
||||
if t == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if selectedMessage != nil {
|
||||
inputWidget.SetTitle("")
|
||||
go session.ChannelMessageSendReply(
|
||||
selectedMessage.ChannelID,
|
||||
t,
|
||||
selectedMessage.Reference(),
|
||||
)
|
||||
|
||||
selectedMessage = nil
|
||||
} else {
|
||||
go session.ChannelMessageSend(selectedChannel.ID, t)
|
||||
}
|
||||
|
||||
inputWidget.SetText("")
|
||||
case tcell.KeyCtrlV:
|
||||
text, _ := clipboard.ReadAll()
|
||||
text = inputWidget.GetText() + text
|
||||
inputWidget.SetText(text)
|
||||
case tcell.KeyEscape:
|
||||
inputWidget.SetTitle("")
|
||||
selectedMessage = nil
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func newSession() *discordgo.Session {
|
||||
s, err := discordgo.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.UserAgent = config.UserAgent
|
||||
s.Identify.Compress = false
|
||||
s.Identify.Intents = 0
|
||||
s.Identify.LargeThreshold = 0
|
||||
s.Identify.Properties.Device = ""
|
||||
s.Identify.Properties.Browser = "Chrome"
|
||||
s.Identify.Properties.OS = "Linux"
|
||||
|
||||
s.AddHandlerOnce(onSessionReady)
|
||||
s.AddHandler(onSessionMessageCreate)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func onSessionReady(_ *discordgo.Session, r *discordgo.Ready) {
|
||||
sort.Slice(r.Guilds, func(a, b int) bool {
|
||||
found := false
|
||||
for _, gID := range r.Settings.GuildPositions {
|
||||
if found {
|
||||
if gID == r.Guilds[b].ID {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if gID == r.Guilds[a].ID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
n := guildsWidget.GetRoot()
|
||||
for _, g := range r.Guilds {
|
||||
gn := tview.NewTreeNode(g.Name).
|
||||
SetReference(g.ID)
|
||||
n.AddChild(gn)
|
||||
}
|
||||
|
||||
guildsWidget.SetCurrentNode(n)
|
||||
}
|
||||
|
||||
func onSessionMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
if selectedChannel == nil {
|
||||
selectedChannel = &discordgo.Channel{ID: ""}
|
||||
}
|
||||
|
||||
if selectedChannel.ID != m.ChannelID {
|
||||
if config.Notifications {
|
||||
for _, u := range m.Mentions {
|
||||
if u.ID == s.State.User.ID {
|
||||
g, err := s.State.Guild(m.GuildID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c, err := s.State.Channel(m.ChannelID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go beeep.Alert(fmt.Sprintf("%s (#%s)", g.Name, c.Name), m.ContentWithMentionsReplaced(), "")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
selectedChannel.Messages = append(selectedChannel.Messages, m.Message)
|
||||
util.WriteMessage(
|
||||
messagesWidget,
|
||||
m.Message,
|
||||
session.State.Ready.User.ID,
|
||||
)
|
||||
}
|
||||
|
||||
func onGuildsWidgetSelected(n *tview.TreeNode) {
|
||||
selectedChannel = nil
|
||||
selectedMessage = nil
|
||||
messagesWidget.
|
||||
Clear().
|
||||
SetTitle("")
|
||||
// Unhighlight the already-highlighted regions.
|
||||
messagesWidget.Highlight()
|
||||
|
||||
switch n.GetLevel() {
|
||||
case 1:
|
||||
if len(n.GetChildren()) != 0 {
|
||||
n.SetExpanded(!n.IsExpanded())
|
||||
return
|
||||
}
|
||||
|
||||
n.ClearChildren()
|
||||
|
||||
gID := n.GetReference().(string)
|
||||
g, err := session.State.Guild(gID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cs := g.Channels
|
||||
sort.Slice(cs, func(i, j int) bool {
|
||||
return cs[i].Position < cs[j].Position
|
||||
})
|
||||
|
||||
// Top-level channels
|
||||
ui.CreateTopLevelChannelsTreeNodes(session.State, n, cs)
|
||||
// Category channels
|
||||
ui.CreateCategoryChannelsTreeNodes(session.State, n, cs)
|
||||
// Second-level channels
|
||||
ui.CreateSecondLevelChannelsTreeNodes(session.State, guildsWidget, cs)
|
||||
default:
|
||||
cID := n.GetReference().(string)
|
||||
c, err := session.State.Channel(cID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if c.Type == discordgo.ChannelTypeGuildCategory {
|
||||
n.SetExpanded(!n.IsExpanded())
|
||||
} else if c.Type == discordgo.ChannelTypeGuildNews || c.Type == discordgo.ChannelTypeGuildText {
|
||||
selectedChannel = c
|
||||
app.SetFocus(inputWidget)
|
||||
|
||||
title := "#" + c.Name
|
||||
if c.Topic != "" {
|
||||
title += " - " + c.Topic
|
||||
}
|
||||
messagesWidget.
|
||||
Clear().
|
||||
SetTitle(title)
|
||||
|
||||
go writeMessages(c.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeMessages(cID string) {
|
||||
ms, err := session.ChannelMessages(cID, config.GetMessagesLimit, "", "", "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := len(ms) - 1; i >= 0; i-- {
|
||||
selectedChannel.Messages = append(selectedChannel.Messages, ms[i])
|
||||
|
||||
util.WriteMessage(
|
||||
messagesWidget,
|
||||
ms[i],
|
||||
session.State.Ready.User.ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func onLoginFormLoginButtonSelected() {
|
||||
email := loginWidget.GetFormItem(0).(*tview.InputField).GetText()
|
||||
password := loginWidget.GetFormItem(1).(*tview.InputField).GetText()
|
||||
if email == "" || password == "" {
|
||||
return
|
||||
}
|
||||
|
||||
session = newSession()
|
||||
// Login using the email and password
|
||||
lr, err := util.Login(session, email, password)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if lr.Token != "" && !lr.MFA {
|
||||
app.
|
||||
SetRoot(mainFlex, true).
|
||||
SetFocus(guildsWidget)
|
||||
|
||||
session.Token = lr.Token
|
||||
session.Identify.Token = lr.Token
|
||||
if err = session.Open(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go keyring.Set("discordo", "token", lr.Token)
|
||||
} else if lr.MFA {
|
||||
// The account has MFA enabled, reattempt login with code and ticket.
|
||||
loginWidget = ui.NewLoginWidget(func() {
|
||||
code := loginWidget.GetFormItem(0).(*tview.InputField).GetText()
|
||||
if code == "" {
|
||||
return
|
||||
}
|
||||
|
||||
lr, err = util.TOTP(session, code, lr.Ticket)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.
|
||||
SetRoot(mainFlex, true).
|
||||
SetFocus(guildsWidget)
|
||||
|
||||
session.Token = lr.Token
|
||||
session.Identify.Token = lr.Token
|
||||
if err = session.Open(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go keyring.Set("discordo", "token", lr.Token)
|
||||
}, true)
|
||||
|
||||
app.SetRoot(loginWidget, true)
|
||||
}
|
||||
}
|
||||
140
main.go
Normal file
140
main.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
var (
|
||||
app *tview.Application
|
||||
loginForm *tview.Form
|
||||
guildsTreeView *tview.TreeView
|
||||
messagesTextView *tview.TextView
|
||||
messageInputField *tview.InputField
|
||||
mainFlex *tview.Flex
|
||||
|
||||
conf *config
|
||||
session *discordgo.Session
|
||||
selectedChannel *discordgo.Channel
|
||||
selectedMessage *discordgo.Message
|
||||
)
|
||||
|
||||
func main() {
|
||||
conf = loadConfig()
|
||||
tview.Styles = conf.Theme
|
||||
|
||||
app = tview.NewApplication()
|
||||
app.
|
||||
EnableMouse(conf.Mouse).
|
||||
SetInputCapture(onAppInputCapture)
|
||||
|
||||
guildsTreeView = newGuildsTreeView()
|
||||
messagesTextView = newMessagesTextView()
|
||||
messageInputField = newMessageInputField()
|
||||
|
||||
rightFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(messagesTextView, 0, 1, false).
|
||||
AddItem(messageInputField, 3, 1, false)
|
||||
mainFlex = tview.NewFlex().
|
||||
AddItem(guildsTreeView, 0, 1, false).
|
||||
AddItem(rightFlex, 0, 4, false)
|
||||
|
||||
token := conf.Token
|
||||
if t, _ := keyring.Get("discordo", "token"); t != "" {
|
||||
token = t
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
app.
|
||||
SetRoot(mainFlex, true).
|
||||
SetFocus(guildsTreeView)
|
||||
|
||||
session = newSession()
|
||||
session.Token = token
|
||||
session.Identify.Token = token
|
||||
if err := session.Open(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
loginForm = newLoginForm(onLoginFormLoginButtonSelected, false)
|
||||
app.SetRoot(loginForm, true)
|
||||
}
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
if e.Modifiers() == tcell.ModAlt {
|
||||
switch e.Rune() {
|
||||
case '1':
|
||||
app.SetFocus(guildsTreeView)
|
||||
case '2':
|
||||
app.SetFocus(messagesTextView)
|
||||
case '3':
|
||||
app.SetFocus(messageInputField)
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func onLoginFormLoginButtonSelected() {
|
||||
email := loginForm.GetFormItem(0).(*tview.InputField).GetText()
|
||||
password := loginForm.GetFormItem(1).(*tview.InputField).GetText()
|
||||
if email == "" || password == "" {
|
||||
return
|
||||
}
|
||||
|
||||
session = newSession()
|
||||
// Login using the email and password
|
||||
lr, err := login(session, email, password)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if lr.Token != "" && !lr.MFA {
|
||||
app.
|
||||
SetRoot(mainFlex, true).
|
||||
SetFocus(guildsTreeView)
|
||||
|
||||
session.Token = lr.Token
|
||||
session.Identify.Token = lr.Token
|
||||
if err = session.Open(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go keyring.Set("discordo", "token", lr.Token)
|
||||
} else if lr.MFA {
|
||||
// The account has MFA enabled, reattempt login with code and ticket.
|
||||
loginForm = newLoginForm(func() {
|
||||
code := loginForm.GetFormItem(0).(*tview.InputField).GetText()
|
||||
if code == "" {
|
||||
return
|
||||
}
|
||||
|
||||
lr, err = totp(session, code, lr.Ticket)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.
|
||||
SetRoot(mainFlex, true).
|
||||
SetFocus(guildsTreeView)
|
||||
|
||||
session.Token = lr.Token
|
||||
session.Identify.Token = lr.Token
|
||||
if err = session.Open(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go keyring.Set("discordo", "token", lr.Token)
|
||||
}, true)
|
||||
|
||||
app.SetRoot(loginForm, true)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,25 @@
|
|||
package util
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// WriteMessage parses, renders, and writes a message to the given TextView.
|
||||
func WriteMessage(v *tview.TextView, m *discordgo.Message, clientID string) {
|
||||
func renderMessages(cID string) {
|
||||
ms, err := session.ChannelMessages(cID, conf.GetMessagesLimit, "", "", "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := len(ms) - 1; i >= 0; i-- {
|
||||
selectedChannel.Messages = append(selectedChannel.Messages, ms[i])
|
||||
renderMessage(ms[i])
|
||||
}
|
||||
}
|
||||
|
||||
func renderMessage(m *discordgo.Message) {
|
||||
var b strings.Builder
|
||||
|
||||
switch m.Type {
|
||||
|
|
@ -26,25 +35,21 @@ func WriteMessage(v *tview.TextView, m *discordgo.Message, clientID string) {
|
|||
if rm := m.ReferencedMessage; rm != nil {
|
||||
b.WriteString(" ╭ ")
|
||||
b.WriteString("[::d]")
|
||||
parseAuthor(&b, rm.Author, clientID)
|
||||
parseAuthor(&b, rm.Author)
|
||||
|
||||
if rm.Content != "" {
|
||||
rm.Content = parseMentions(
|
||||
rm.Content,
|
||||
rm.Mentions,
|
||||
clientID,
|
||||
)
|
||||
rm.Content = parseMentions(rm.Content, rm.Mentions)
|
||||
b.WriteString(rm.Content)
|
||||
}
|
||||
|
||||
b.WriteString("[::-]\n")
|
||||
}
|
||||
// Render the author of the message.
|
||||
parseAuthor(&b, m.Author, clientID)
|
||||
parseAuthor(&b, m.Author)
|
||||
// If the message content is not empty, parse the message mentions
|
||||
// (users mentioned in the message) and render the message content.
|
||||
if m.Content != "" {
|
||||
m.Content = parseMentions(m.Content, m.Mentions, clientID)
|
||||
m.Content = parseMentions(m.Content, m.Mentions)
|
||||
b.WriteString(m.Content)
|
||||
}
|
||||
// If the edited timestamp of the message is not empty; it implies that
|
||||
|
|
@ -68,24 +73,20 @@ func WriteMessage(v *tview.TextView, m *discordgo.Message, clientID string) {
|
|||
// therefore be used to mark the end of a region.
|
||||
b.WriteString("[\"\"]")
|
||||
|
||||
fmt.Fprintln(v, b.String())
|
||||
fmt.Fprintln(messagesTextView, b.String())
|
||||
case discordgo.MessageTypeGuildMemberJoin:
|
||||
b.WriteString("[#5865F2]")
|
||||
b.WriteString(m.Author.Username)
|
||||
b.WriteString("[-] joined the server")
|
||||
|
||||
fmt.Fprintln(v, b.String())
|
||||
fmt.Fprintln(messagesTextView, b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func parseMentions(
|
||||
content string,
|
||||
mentions []*discordgo.User,
|
||||
clientID string,
|
||||
) string {
|
||||
func parseMentions(content string, mentions []*discordgo.User) string {
|
||||
for _, mUser := range mentions {
|
||||
var color string
|
||||
if mUser.ID == clientID {
|
||||
if mUser.ID == session.State.User.ID {
|
||||
color = "[:#5865F2]"
|
||||
} else {
|
||||
color = "[#EB459E]"
|
||||
|
|
@ -104,10 +105,8 @@ func parseMentions(
|
|||
return content
|
||||
}
|
||||
|
||||
func parseAuthor(b *strings.Builder, u *discordgo.User, clientID string) {
|
||||
// If the message author is the client, modify the text color for
|
||||
// distinction.
|
||||
if u.ID == clientID {
|
||||
func parseAuthor(b *strings.Builder, u *discordgo.User) {
|
||||
if u.ID == session.State.User.ID {
|
||||
b.WriteString("[#57F287]")
|
||||
} else {
|
||||
b.WriteString("[#ED4245]")
|
||||
|
|
@ -121,61 +120,3 @@ func parseAuthor(b *strings.Builder, u *discordgo.User, clientID string) {
|
|||
b.WriteString("[#EB459E]BOT[-] ")
|
||||
}
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
MFA bool `json:"mfa"`
|
||||
SMS bool `json:"sms"`
|
||||
Ticket string `json:"ticket"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// Login creates a new request to the `/login` endpoint for essential login
|
||||
// information.
|
||||
func Login(
|
||||
s *discordgo.Session,
|
||||
email, password string,
|
||||
) (*loginResponse, error) {
|
||||
data := struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}{email, password}
|
||||
resp, err := s.RequestWithBucketID(
|
||||
"POST",
|
||||
discordgo.EndpointLogin,
|
||||
data,
|
||||
discordgo.EndpointLogin,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lr loginResponse
|
||||
err = json.Unmarshal(resp, &lr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &lr, nil
|
||||
}
|
||||
|
||||
// TOTP creates a new request to `/mfa/totp` endpoint for time-based one-time
|
||||
// passcode for essential login information
|
||||
func TOTP(s *discordgo.Session, code, ticket string) (*loginResponse, error) {
|
||||
data := struct {
|
||||
Code string `json:"code"`
|
||||
Ticket string `json:"ticket"`
|
||||
}{code, ticket}
|
||||
e := discordgo.EndpointAuth + "mfa/totp"
|
||||
resp, err := s.RequestWithBucketID("POST", e, data, e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lr loginResponse
|
||||
err = json.Unmarshal(resp, &lr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &lr, nil
|
||||
}
|
||||
363
ui.go
Normal file
363
ui.go
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func newGuildsTreeView() *tview.TreeView {
|
||||
w := tview.NewTreeView()
|
||||
w.
|
||||
SetSelectedFunc(onGuildsTreeViewSelected).
|
||||
SetTopLevel(1).
|
||||
SetRoot(tview.NewTreeNode("")).
|
||||
SetBorder(true).
|
||||
SetBorderPadding(0, 0, 1, 0).
|
||||
SetTitle("Guilds").
|
||||
SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func onGuildsTreeViewSelected(n *tview.TreeNode) {
|
||||
selectedChannel = nil
|
||||
selectedMessage = nil
|
||||
messagesTextView.
|
||||
Clear().
|
||||
SetTitle("")
|
||||
messageInputField.SetText("")
|
||||
// Unhighlight the already-highlighted regions.
|
||||
messagesTextView.Highlight()
|
||||
|
||||
switch n.GetLevel() {
|
||||
case 1:
|
||||
if len(n.GetChildren()) != 0 {
|
||||
n.SetExpanded(!n.IsExpanded())
|
||||
return
|
||||
}
|
||||
|
||||
n.ClearChildren()
|
||||
|
||||
gID := n.GetReference().(string)
|
||||
g, err := session.State.Guild(gID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cs := g.Channels
|
||||
sort.Slice(cs, func(i, j int) bool {
|
||||
return cs[i].Position < cs[j].Position
|
||||
})
|
||||
|
||||
// Top-level channels
|
||||
createTopLevelChannelsTreeNodes(n, cs)
|
||||
// Category channels
|
||||
createCategoryChannelsTreeNodes(n, cs)
|
||||
// Second-level channels
|
||||
createSecondLevelChannelsTreeNodes(cs)
|
||||
default:
|
||||
cID := n.GetReference().(string)
|
||||
c, err := session.State.Channel(cID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if c.Type == discordgo.ChannelTypeGuildCategory {
|
||||
n.SetExpanded(!n.IsExpanded())
|
||||
} else if c.Type == discordgo.ChannelTypeGuildNews || c.Type == discordgo.ChannelTypeGuildText {
|
||||
selectedChannel = c
|
||||
app.SetFocus(messageInputField)
|
||||
|
||||
title := "#" + c.Name
|
||||
if c.Topic != "" {
|
||||
title += " - " + c.Topic
|
||||
}
|
||||
messagesTextView.
|
||||
Clear().
|
||||
SetTitle(title)
|
||||
|
||||
go renderMessages(c.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTextChannelTreeNode(c *discordgo.Channel) *tview.TreeNode {
|
||||
n := tview.NewTreeNode("[::d]#" + c.Name + "[::-]").
|
||||
SetReference(c.ID)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func createTopLevelChannelsTreeNodes(
|
||||
n *tview.TreeNode,
|
||||
cs []*discordgo.Channel,
|
||||
) {
|
||||
for _, c := range cs {
|
||||
if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
|
||||
(c.ParentID == "") {
|
||||
if p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
|
||||
continue
|
||||
}
|
||||
|
||||
cn := newTextChannelTreeNode(c)
|
||||
n.AddChild(cn)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createCategoryChannelsTreeNodes(
|
||||
n *tview.TreeNode,
|
||||
cs []*discordgo.Channel,
|
||||
) {
|
||||
CategoryLoop:
|
||||
for _, c := range cs {
|
||||
if c.Type == discordgo.ChannelTypeGuildCategory {
|
||||
if p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, child := range cs {
|
||||
if child.ParentID == c.ID {
|
||||
cn := tview.NewTreeNode(c.Name).
|
||||
SetReference(c.ID)
|
||||
n.AddChild(cn)
|
||||
continue CategoryLoop
|
||||
}
|
||||
}
|
||||
|
||||
cn := tview.NewTreeNode(c.Name).
|
||||
SetReference(c.ID)
|
||||
n.AddChild(cn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createSecondLevelChannelsTreeNodes(cs []*discordgo.Channel) {
|
||||
for _, c := range cs {
|
||||
if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
|
||||
(c.ParentID != "") {
|
||||
if p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
|
||||
continue
|
||||
}
|
||||
|
||||
if pn := getTreeNodeByReference(c.ParentID); pn != nil {
|
||||
cn := newTextChannelTreeNode(c)
|
||||
pn.AddChild(cn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTreeNodeByReference(r interface{}) (mn *tview.TreeNode) {
|
||||
guildsTreeView.GetRoot().Walk(func(n, _ *tview.TreeNode) bool {
|
||||
if n.GetReference() == r {
|
||||
mn = n
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func newMessagesTextView() *tview.TextView {
|
||||
w := tview.NewTextView()
|
||||
w.
|
||||
SetRegions(true).
|
||||
SetDynamicColors(true).
|
||||
SetWordWrap(true).
|
||||
ScrollToEnd().
|
||||
SetChangedFunc(func() {
|
||||
app.Draw()
|
||||
}).
|
||||
SetInputCapture(onMessagesTextViewInputCapture).
|
||||
SetBorder(true).
|
||||
SetBorderPadding(0, 0, 1, 0).
|
||||
SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func findByMessageID(ms []*discordgo.Message, mID string) (int, *discordgo.Message) {
|
||||
for i, m := range ms {
|
||||
if mID == m.ID {
|
||||
return i, m
|
||||
}
|
||||
}
|
||||
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func onMessagesTextViewInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
if selectedChannel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case e.Key() == tcell.KeyUp || e.Rune() == 'k': // Up
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hs := messagesTextView.GetHighlights()
|
||||
if len(hs) == 0 {
|
||||
messagesTextView.
|
||||
Highlight(ms[len(ms)-1].ID).
|
||||
ScrollToHighlight()
|
||||
} else {
|
||||
idx, _ := findByMessageID(ms, hs[0])
|
||||
if idx == -1 || idx == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
messagesTextView.
|
||||
Highlight(ms[idx-1].ID).
|
||||
ScrollToHighlight()
|
||||
}
|
||||
|
||||
return nil
|
||||
case e.Key() == tcell.KeyDown || e.Rune() == 'j': // Down
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hs := messagesTextView.GetHighlights()
|
||||
if len(hs) == 0 {
|
||||
messagesTextView.
|
||||
Highlight(ms[len(ms)-1].ID).
|
||||
ScrollToHighlight()
|
||||
} else {
|
||||
idx, _ := findByMessageID(ms, hs[0])
|
||||
if idx == -1 || idx == len(ms)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
messagesTextView.
|
||||
Highlight(ms[idx+1].ID).
|
||||
ScrollToHighlight()
|
||||
}
|
||||
|
||||
return nil
|
||||
case e.Key() == tcell.KeyHome || e.Rune() == 'g': // Top
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
messagesTextView.
|
||||
Highlight(ms[0].ID).
|
||||
ScrollToHighlight()
|
||||
case e.Key() == tcell.KeyEnd || e.Rune() == 'G': // Bottom
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
messagesTextView.
|
||||
Highlight(ms[len(ms)-1].ID).
|
||||
ScrollToHighlight()
|
||||
case e.Rune() == 'r': // Inline reply
|
||||
ms := selectedChannel.Messages
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hs := messagesTextView.GetHighlights()
|
||||
if len(hs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, selectedMessage = findByMessageID(ms, hs[0])
|
||||
messageInputField.SetTitle(
|
||||
"Replying to " + selectedMessage.Author.Username,
|
||||
)
|
||||
app.SetFocus(messageInputField)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func newMessageInputField() *tview.InputField {
|
||||
w := tview.NewInputField()
|
||||
w.
|
||||
SetPlaceholder("Message...").
|
||||
SetPlaceholderTextColor(tcell.ColorWhite).
|
||||
SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor).
|
||||
SetInputCapture(onMessageInputFieldInputCapture).
|
||||
SetBorder(true).
|
||||
SetBorderPadding(0, 0, 1, 0).
|
||||
SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
// If the "Alt" modifier key is pressed, do not handle the event.
|
||||
if e.Modifiers() == tcell.ModAlt {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch e.Key() {
|
||||
case tcell.KeyEnter:
|
||||
if selectedChannel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
t := strings.TrimSpace(messageInputField.GetText())
|
||||
if t == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if selectedMessage != nil {
|
||||
messageInputField.SetTitle("")
|
||||
go session.ChannelMessageSendReply(
|
||||
selectedMessage.ChannelID,
|
||||
t,
|
||||
selectedMessage.Reference(),
|
||||
)
|
||||
|
||||
selectedMessage = nil
|
||||
} else {
|
||||
go session.ChannelMessageSend(selectedChannel.ID, t)
|
||||
}
|
||||
|
||||
messageInputField.SetText("")
|
||||
case tcell.KeyCtrlV:
|
||||
text, _ := clipboard.ReadAll()
|
||||
text = messageInputField.GetText() + text
|
||||
messageInputField.SetText(text)
|
||||
case tcell.KeyEscape:
|
||||
messageInputField.SetTitle("")
|
||||
selectedMessage = nil
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func newLoginForm(onLoginFormLoginButtonSelected func(), mfa bool) *tview.Form {
|
||||
w := tview.NewForm()
|
||||
w.
|
||||
AddButton("Login", onLoginFormLoginButtonSelected).
|
||||
SetButtonsAlign(tview.AlignCenter).
|
||||
SetBorder(true).
|
||||
SetBorderPadding(0, 0, 1, 0)
|
||||
|
||||
if mfa {
|
||||
w.AddPasswordField("Code", "", 0, 0, nil)
|
||||
} else {
|
||||
w.
|
||||
AddInputField("Email", "", 0, nil, nil).
|
||||
AddPasswordField("Password", "", 0, 0, nil)
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
119
ui/guilds.go
119
ui/guilds.go
|
|
@ -1,119 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// NewGuildsWidget creates and returns a new guilds widget.
|
||||
func NewGuildsWidget() *tview.TreeView {
|
||||
w := tview.NewTreeView()
|
||||
w.
|
||||
SetTopLevel(1).
|
||||
SetRoot(tview.NewTreeNode("")).
|
||||
SetBorder(true).
|
||||
SetBorderPadding(0, 0, 1, 0).
|
||||
SetTitle("Guilds").
|
||||
SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// NewTextChannelTreeNode creates and returns a new text channel treenode.
|
||||
func NewTextChannelTreeNode(c *discordgo.Channel) *tview.TreeNode {
|
||||
n := tview.NewTreeNode("[::d]#" + c.Name + "[::-]").
|
||||
SetReference(c.ID)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// GetTreeNodeByReference gets the TreeNode that has reference r from the given
|
||||
// treeview.
|
||||
func GetTreeNodeByReference(
|
||||
r interface{},
|
||||
treeV *tview.TreeView,
|
||||
) (mn *tview.TreeNode) {
|
||||
treeV.GetRoot().Walk(func(n, _ *tview.TreeNode) bool {
|
||||
if n.GetReference() == r {
|
||||
mn = n
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateTopLevelChannelsTreeNodes creates TreeNodes for the top-level (orphan)
|
||||
// channels.
|
||||
func CreateTopLevelChannelsTreeNodes(
|
||||
s *discordgo.State,
|
||||
n *tview.TreeNode,
|
||||
cs []*discordgo.Channel,
|
||||
) {
|
||||
for _, c := range cs {
|
||||
if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
|
||||
(c.ParentID == "") {
|
||||
if p, err := s.UserChannelPermissions(s.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
|
||||
continue
|
||||
}
|
||||
|
||||
cn := NewTextChannelTreeNode(c)
|
||||
n.AddChild(cn)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCategoryChannelsTreeNodes creates TreeNodes for the category (parent)
|
||||
// channels.
|
||||
func CreateCategoryChannelsTreeNodes(
|
||||
s *discordgo.State,
|
||||
n *tview.TreeNode,
|
||||
cs []*discordgo.Channel,
|
||||
) {
|
||||
CategoryLoop:
|
||||
for _, c := range cs {
|
||||
if c.Type == discordgo.ChannelTypeGuildCategory {
|
||||
if p, err := s.UserChannelPermissions(s.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, child := range cs {
|
||||
if child.ParentID == c.ID {
|
||||
cn := tview.NewTreeNode(c.Name).
|
||||
SetReference(c.ID)
|
||||
n.AddChild(cn)
|
||||
continue CategoryLoop
|
||||
}
|
||||
}
|
||||
|
||||
cn := tview.NewTreeNode(c.Name).
|
||||
SetReference(c.ID)
|
||||
n.AddChild(cn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSecondLevelChannelsTreeNodes creates TreeNodes for the second-level
|
||||
// (category children) channels.
|
||||
func CreateSecondLevelChannelsTreeNodes(
|
||||
s *discordgo.State,
|
||||
treeV *tview.TreeView,
|
||||
cs []*discordgo.Channel,
|
||||
) {
|
||||
for _, c := range cs {
|
||||
if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
|
||||
(c.ParentID != "") {
|
||||
if p, err := s.UserChannelPermissions(s.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
|
||||
continue
|
||||
}
|
||||
|
||||
if pn := GetTreeNodeByReference(c.ParentID, treeV); pn != nil {
|
||||
cn := NewTextChannelTreeNode(c)
|
||||
pn.AddChild(cn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// NewMessagesWidget creates and returns a new messages widget.
|
||||
func NewMessagesWidget(
|
||||
app *tview.Application,
|
||||
) *tview.TextView {
|
||||
w := tview.NewTextView()
|
||||
w.
|
||||
SetRegions(true).
|
||||
SetDynamicColors(true).
|
||||
SetWordWrap(true).
|
||||
ScrollToEnd().
|
||||
SetChangedFunc(func() {
|
||||
app.Draw()
|
||||
}).
|
||||
SetBorder(true).
|
||||
SetBorderPadding(0, 0, 1, 0).
|
||||
SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// NewInputWidget creates and returns a new input widget.
|
||||
func NewInputWidget() *tview.InputField {
|
||||
w := tview.NewInputField()
|
||||
w.
|
||||
SetPlaceholder("Message...").
|
||||
SetPlaceholderTextColor(tcell.ColorWhite).
|
||||
SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor).
|
||||
SetBorder(true).
|
||||
SetBorderPadding(0, 0, 1, 0).
|
||||
SetTitleAlign(tview.AlignLeft)
|
||||
|
||||
return w
|
||||
}
|
||||
40
ui/misc.go
40
ui/misc.go
|
|
@ -1,40 +0,0 @@
|
|||
package ui
|
||||
|
||||
import "github.com/rivo/tview"
|
||||
|
||||
// NewMainFlex creates and returns a new main flex.
|
||||
func NewMainFlex(
|
||||
treeV *tview.TreeView,
|
||||
textV *tview.TextView,
|
||||
i *tview.InputField,
|
||||
) *tview.Flex {
|
||||
rf := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(textV, 0, 1, false).
|
||||
AddItem(i, 3, 1, false)
|
||||
mf := tview.NewFlex().
|
||||
AddItem(treeV, 0, 1, false).
|
||||
AddItem(rf, 0, 4, false)
|
||||
|
||||
return mf
|
||||
}
|
||||
|
||||
// NewLoginWidget creates and returns a new login widget.
|
||||
func NewLoginWidget(onLoginFormLoginButtonSelected func(), mfa bool) *tview.Form {
|
||||
w := tview.NewForm()
|
||||
w.
|
||||
AddButton("Login", onLoginFormLoginButtonSelected).
|
||||
SetButtonsAlign(tview.AlignCenter).
|
||||
SetBorder(true).
|
||||
SetBorderPadding(0, 0, 1, 0)
|
||||
|
||||
if mfa {
|
||||
w.AddPasswordField("Code", "", 0, 0, nil)
|
||||
} else {
|
||||
w.
|
||||
AddInputField("Email", "", 0, nil, nil).
|
||||
AddPasswordField("Password", "", 0, 0, nil)
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue