Switch to a flat project structure (#48)

This commit is contained in:
ayntgl 2021-09-19 19:56:51 +04:00 • committed by GitHub
parent 8c27d2b138
commit f945fbf4f8
9 changed files with 676 additions and 745 deletions

View file

@ -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
View 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
}

View file

@ -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
View 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)
}
}

View file

@ -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
View 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
}

View file

@ -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)
}
}
}
}

View file

@ -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
}

View file

@ -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
}