diff --git a/.github/preview.png b/.github/preview.png index e471ee8..d893b42 100644 Binary files a/.github/preview.png and b/.github/preview.png differ diff --git a/config/config.go b/config/config.go index 91b6ac1..4b19b7d 100644 --- a/config/config.go +++ b/config/config.go @@ -16,10 +16,10 @@ type IdentifyConfig struct { } type KeysConfig struct { - ToggleGuildsList string `toml:"toggle_guilds_list"` - ToggleChannelsTreeView string `toml:"toggle_channels_tree_view"` - ToggleMessagesTextView string `toml:"toggle_messages_text_view"` - ToggleMessageInputField string `toml:"toggle_message_input_field"` + ToggleGuildsTree string `toml:"toggle_guilds_tree"` + ToggleChannelsTree string `toml:"toggle_channels_tree"` + ToggleMessagesTextView string `toml:"toggle_messages_text_view"` + ToggleMessageInput string `toml:"toggle_message_input"` OpenMessageActionsList string `toml:"open_message_actions_list"` OpenExternalEditor string `toml:"open_external_editor"` @@ -39,7 +39,7 @@ type ThemeConfig struct { type Config struct { Mouse bool `toml:"mouse"` Timestamps bool `toml:"timestamps"` - MessagesLimit int `toml:"messages_limit"` + MessagesLimit uint `toml:"messages_limit"` Timezone string `toml:"timezone"` AttachmentDownloadsDir string `toml:"attachment_downloads_dir"` Identify IdentifyConfig `toml:"identify"` @@ -65,10 +65,10 @@ func New() *Config { Title: "white", }, Keys: KeysConfig{ - ToggleGuildsList: "Rune[g]", - ToggleChannelsTreeView: "Rune[c]", - ToggleMessagesTextView: "Rune[m]", - ToggleMessageInputField: "Rune[i]", + ToggleGuildsTree: "Rune[g]", + ToggleChannelsTree: "Rune[c]", + ToggleMessagesTextView: "Rune[m]", + ToggleMessageInput: "Rune[i]", OpenMessageActionsList: "Rune[a]", OpenExternalEditor: "Ctrl+E", diff --git a/discord/markdown_test.go b/discord/markdown_test.go deleted file mode 100644 index e27b94a..0000000 --- a/discord/markdown_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package discord - -import "testing" - -func TestParseMarkdown(t *testing.T) { - tests := []struct { - name string - in string - want string - }{ - {"bold", "**test**", "[::b]test[::-]"}, - {"italic", "*test*", "[::i]test[::-]"}, - {"underline", "__test__", "[::u]test[::-]"}, - {"strikethrough", "~~test~~", "[::s]test[::-]"}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if got := ParseMarkdown(test.in); got != test.want { - t.Errorf("got: %s\nwant: %s", got, test.want) - } - }) - } -} - -func BenchmarkParseMarkdown(b *testing.B) { - in := `**Porro mollitia aut odio dolor rerum.** Saepe qui aut reiciendis illo nisi. Id illo et quo consequatur sint labore placeat maiores. __Commodi odio quae reprehenderit.__ Beatae illum est fugiat ut architecto itaque eveniet aut. ~~Consequuntur quas explicabo et impedit eum porro facere et.~~ - - Sit commodi sed iure et sed quae eveniet. *Sit non distinctio nihil sunt. Nesciunt cumque aspernatur *nulla* porro et earum quidem.* Sed omnis at commodi vel quasi. Fuga et **consequatur** molestias dicta vel provident et aspernatur. Dolorem molestias ipsa aut ~~facilis quae dolorem~~ eveniet dicta.` - - for i := 0; i < b.N; i++ { - ParseMarkdown(in) - } -} diff --git a/discord/util.go b/discord/util.go deleted file mode 100644 index ceb5899..0000000 --- a/discord/util.go +++ /dev/null @@ -1,45 +0,0 @@ -package discord - -import ( - "strings" - - "github.com/ayntgl/astatine" -) - -func ChannelToString(c *astatine.Channel) string { - var repr string - if c.Name != "" { - repr = "#" + c.Name - } else if len(c.Recipients) == 1 { - rp := c.Recipients[0] - repr = rp.Username + "#" + rp.Discriminator - } else { - rps := make([]string, len(c.Recipients)) - for i, r := range c.Recipients { - rps[i] = r.Username + "#" + r.Discriminator - } - - repr = strings.Join(rps, ", ") - } - - return repr -} - -func FindMessageByID(ms []*astatine.Message, mID string) (int, *astatine.Message) { - for i, m := range ms { - if m.ID == mID { - return i, m - } - } - - return -1, nil -} - -func HasPermission(s *astatine.State, cID string, p int64) bool { - perm, err := s.UserChannelPermissions(s.User.ID, cID) - if err != nil { - return false - } - - return perm&p == p -} diff --git a/go.mod b/go.mod index dde1e09..1134b43 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/BurntSushi/toml v1.1.0 github.com/atotto/clipboard v0.1.4 - github.com/ayntgl/astatine v0.24.1-0.20220324085605-e85d32085ce8 + github.com/diamondburned/arikawa/v3 v3.0.0 github.com/gdamore/tcell/v2 v2.5.1 github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 @@ -20,13 +20,15 @@ require ( github.com/danieljoos/wincred v1.1.2 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gorilla/schema v1.2.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect ) diff --git a/go.sum b/go.sum index 71c0418..9e30cfd 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,3 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= -github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= @@ -9,35 +6,34 @@ github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0 h1:R/qAiUxFT3mNgQ github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0/go.mod h1:v3ZDlfVAL1OrkKHbGSFFK60k0/7hruHPDq2XMs9Gu6U= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/ayntgl/astatine v0.24.1-0.20220324085605-e85d32085ce8 h1:tn/I/8Zv22Q8ERV/awDfLTSKQR5eH5AAYpzCIuRUaqw= -github.com/ayntgl/astatine v0.24.1-0.20220324085605-e85d32085ce8/go.mod h1:xRC0h8PGhVmnvNh2eKSkJJ4xvtcQEZpvKmepqRMbNEU= -github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/diamondburned/arikawa/v3 v3.0.0 h1:VbdX1DtrBLE752IJftZHInVy6v8I3T8vhN9rKGvO6AY= +github.com/diamondburned/arikawa/v3 v3.0.0/go.mod h1:5jBSNnp82Z/EhsKa6Wk9FsOqSxfVkNZDTDBPOj47LpY= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= -github.com/gdamore/tcell/v2 v2.4.1-0.20220313203054-2a1a1b586447 h1:4idf9699cuWAc7ZIB+2RzuDWU30oRkB0X/FZTUlWOVY= -github.com/gdamore/tcell/v2 v2.4.1-0.20220313203054-2a1a1b586447/go.mod h1:I8YJFI9gzgl4dHi9UlRDZosCW+jYkDA37AXmXvL51w4= github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I= github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= -github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc= @@ -50,44 +46,38 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I= -github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= github.com/urfave/cli/v2 v2.7.1 h1:DsAOFeI9T0vmUW4LiGR5mhuCIn5kqGIE4WMU2ytmH00= github.com/urfave/cli/v2 v2.7.1/go.mod h1:TYFbtzt/azQoJOrGH5mDfZtS0jIkl/OeFwlRWPR9KRM= github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc= github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw= -golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s= -golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0= -golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 h1:7NCfEGl0sfUojmX78nK9pBJuUlSZWEJA/TwASvfiPLo= -golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211001092434-39dca1131b70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 035268b..53adae3 100644 --- a/main.go +++ b/main.go @@ -45,7 +45,7 @@ func main() { c := config.New() err := c.Load(ctx.String("config")) if err != nil { - log.Fatal(err) + return err } token := ctx.String("token") @@ -53,11 +53,11 @@ func main() { if token != "" { err := app.Connect() if err != nil { - panic(err) + return err } app.DrawMainFlex() - app.SetFocus(app.GuildsList) + app.SetFocus(app.GuildsTree) } else { loginForm := ui.NewLoginForm(false) loginForm.AddButton("Login", func() { @@ -67,21 +67,21 @@ func main() { return } - // Login using the email and password - lr, err := app.Session.Login(email, password) + // Login using the email and password only + lr, err := app.State.Login(email, password) if err != nil { - panic(err) + log.Fatal(err) } - if lr.Token != "" && !lr.Mfa { - app.Session.Identify.Token = lr.Token + if lr.Token != "" && !lr.MFA { + app.State.Token = lr.Token err = app.Connect() if err != nil { - panic(err) + log.Fatal(err) } app.DrawMainFlex() - app.SetFocus(app.GuildsList) + app.SetFocus(app.GuildsTree) go keyring.Set(name, "token", lr.Token) } else { @@ -93,19 +93,19 @@ func main() { return } - lr, err = app.Session.Totp(code, lr.Ticket) + lr, err = app.State.TOTP(code, lr.Ticket) if err != nil { - panic(err) + log.Fatal(err) } - app.Session.Identify.Token = lr.Token + app.State.Token = lr.Token err = app.Connect() if err != nil { - panic(err) + log.Fatal(err) } app.DrawMainFlex() - app.SetFocus(app.GuildsList) + app.SetFocus(app.GuildsTree) go keyring.Set(name, "token", lr.Token) }) diff --git a/ui/app.go b/ui/app.go index 832b189..cef4848 100644 --- a/ui/app.go +++ b/ui/app.go @@ -1,11 +1,15 @@ package ui import ( + "context" "sort" "strings" - "github.com/ayntgl/astatine" "github.com/ayntgl/discordo/config" + "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/rivo/tview" ) @@ -13,28 +17,39 @@ import ( type App struct { *tview.Application MainFlex *tview.Flex - GuildsList *GuildsList - ChannelsTreeView *ChannelsTreeView + GuildsTree *GuildsTree + ChannelsTree *ChannelsTree MessagesTextView *MessagesTextView - MessageInputField *MessageInputField - Session *astatine.Session - SelectedChannel *astatine.Channel - Config *config.Config - SelectedMessage int + MessageInputField *MessageInput + + Config *config.Config + State *state.State + SelectedChannel *discord.Channel + SelectedMessage int } func NewApp(token string, c *config.Config) *App { app := &App{ - MainFlex: tview.NewFlex(), - Session: astatine.New(token), - Config: c, + MainFlex: tview.NewFlex(), + Config: c, + + State: state.NewWithIdentifier(gateway.NewIdentifier(gateway.IdentifyCommand{ + Token: token, + Intents: nil, + Properties: gateway.IdentifyProperties{ + OS: c.Identify.Os, + Browser: c.Identify.Browser, + }, + // The official client sets the compress field as false. + Compress: false, + })), SelectedMessage: -1, } - app.GuildsList = NewGuildsList(app) - app.ChannelsTreeView = NewChannelsTreeView(app) + app.GuildsTree = NewGuildsTree(app) + app.ChannelsTree = NewChannelsTree(app) app.MessagesTextView = NewMessagesTextView(app) - app.MessageInputField = NewMessageInputField(app) + app.MessageInputField = NewMessageInput(app) app.Application = tview.NewApplication() app.EnableMouse(app.Config.Mouse) @@ -46,22 +61,15 @@ func NewApp(token string, c *config.Config) *App { func (app *App) Connect() error { // For user accounts, all of the guilds, the user is in, are dispatched in the READY gateway event. // Whereas, for bot accounts, the guilds are dispatched discretely in the GUILD_CREATE gateway events. - if !strings.HasPrefix(app.Session.Identify.Token, "Bot") { - app.Session.UserAgent = app.Config.Identify.UserAgent - app.Session.Identify.Compress = false - app.Session.Identify.LargeThreshold = 0 - app.Session.Identify.Intents = 0 - app.Session.Identify.Properties = astatine.IdentifyProperties{ - OS: app.Config.Identify.Os, - Browser: app.Config.Identify.Browser, - } - app.Session.AddHandlerOnce(app.onSessionReady) + if !strings.HasPrefix(app.State.Token, "Bot") { + api.UserAgent = app.Config.Identify.UserAgent + app.State.AddHandler(app.onStateReady) } - app.Session.AddHandler(app.onSessionGuildCreate) - app.Session.AddHandler(app.onSessionGuildDelete) - app.Session.AddHandler(app.onSessionMessageCreate) - return app.Session.Open() + app.State.AddHandler(app.onStateGuildCreate) + app.State.AddHandler(app.onStateGuildDelete) + app.State.AddHandler(app.onStateMessageCreate) + return app.State.Open(context.Background()) } func (app *App) onInputCapture(e *tcell.EventKey) *tcell.EventKey { @@ -71,16 +79,16 @@ func (app *App) onInputCapture(e *tcell.EventKey) *tcell.EventKey { if app.MainFlex.GetItemCount() != 0 { switch e.Name() { - case app.Config.Keys.ToggleGuildsList: - app.SetFocus(app.GuildsList) + case app.Config.Keys.ToggleGuildsTree: + app.SetFocus(app.GuildsTree) return nil - case app.Config.Keys.ToggleChannelsTreeView: - app.SetFocus(app.ChannelsTreeView) + case app.Config.Keys.ToggleChannelsTree: + app.SetFocus(app.ChannelsTree) return nil case app.Config.Keys.ToggleMessagesTextView: app.SetFocus(app.MessagesTextView) return nil - case app.Config.Keys.ToggleMessageInputField: + case app.Config.Keys.ToggleMessageInput: app.SetFocus(app.MessageInputField) return nil } @@ -92,8 +100,8 @@ func (app *App) onInputCapture(e *tcell.EventKey) *tcell.EventKey { func (app *App) DrawMainFlex() { leftFlex := tview.NewFlex(). SetDirection(tview.FlexRow). - AddItem(app.GuildsList, 10, 1, false). - AddItem(app.ChannelsTreeView, 0, 1, false) + AddItem(app.GuildsTree, 10, 1, false). + AddItem(app.ChannelsTree, 0, 1, false) rightFlex := tview.NewFlex(). SetDirection(tview.FlexRow). AddItem(app.MessagesTextView, 0, 1, false). @@ -105,10 +113,10 @@ func (app *App) DrawMainFlex() { app.SetRoot(app.MainFlex, true) } -func (app *App) onSessionReady(_ *astatine.Session, r *astatine.Ready) { +func (app *App) onStateReady(r *gateway.ReadyEvent) { sort.Slice(r.Guilds, func(a, b int) bool { found := false - for _, guildID := range r.Settings.GuildPositions { + for _, guildID := range r.UserSettings.GuildPositions { if found && guildID == r.Guilds[b].ID { return true } @@ -120,28 +128,48 @@ func (app *App) onSessionReady(_ *astatine.Session, r *astatine.Ready) { return false }) + rootNode := app.GuildsTree.GetRoot() for _, g := range r.Guilds { - app.GuildsList.AddItem(g.Name, "", 0, nil) + guildNode := tview.NewTreeNode(g.Name) + guildNode.SetReference(g.ID) + + rootNode.AddChild(guildNode) } + + app.GuildsTree.SetCurrentNode(rootNode) + app.SetFocus(app.GuildsTree) } -func (app *App) onSessionGuildCreate(_ *astatine.Session, g *astatine.GuildCreate) { - app.GuildsList.AddItem(g.Name, "", 0, nil) +func (app *App) onStateGuildCreate(g *gateway.GuildCreateEvent) { + rootNode := app.GuildsTree.GetRoot() + guildNode := tview.NewTreeNode(g.Name) + guildNode.SetReference(g.ID) + + rootNode.AddChild(guildNode) app.Draw() } -func (app *App) onSessionGuildDelete(_ *astatine.Session, g *astatine.GuildDelete) { - items := app.GuildsList.FindItems(g.BeforeDelete.Name, "", false, false) - if len(items) != 0 { - app.GuildsList.RemoveItem(items[0]) +func (app *App) onStateGuildDelete(g *gateway.GuildDeleteEvent) { + rootNode := app.GuildsTree.GetRoot() + var parentNode *tview.TreeNode + rootNode.Walk(func(node, _ *tview.TreeNode) bool { + if node.GetReference() == g.ID { + parentNode = node + return false + } + + return true + }) + + if parentNode != nil { + rootNode.RemoveChild(parentNode) } app.Draw() } -func (app *App) onSessionMessageCreate(_ *astatine.Session, m *astatine.MessageCreate) { +func (app *App) onStateMessageCreate(m *gateway.MessageCreateEvent) { if app.SelectedChannel != nil && app.SelectedChannel.ID == m.ChannelID { - app.SelectedChannel.Messages = append(app.SelectedChannel.Messages, m.Message) _, err := app.MessagesTextView.Write(buildMessage(app, m.Message)) if err != nil { return diff --git a/ui/builder.go b/ui/builder.go index 1938e79..beb7862 100644 --- a/ui/builder.go +++ b/ui/builder.go @@ -5,23 +5,22 @@ import ( "strings" "time" - "github.com/ayntgl/astatine" - "github.com/ayntgl/discordo/discord" + "github.com/diamondburned/arikawa/v3/discord" ) -func buildMessage(app *App, m *astatine.Message) []byte { +func buildMessage(app *App, m discord.Message) []byte { var b strings.Builder switch m.Type { - case astatine.MessageTypeDefault, astatine.MessageTypeReply: + case discord.DefaultMessage, discord.InlinedReplyMessage: // Define a new region and assign message ID as the region ID. // Learn more: // https://pkg.go.dev/github.com/rivo/tview#hdr-Regions_and_Highlights b.WriteString("[\"") - b.WriteString(m.ID) + b.WriteString(m.ID.String()) b.WriteString("\"]") // Build the message associated with crosspost, channel follow add, pin, or a reply. - buildReferencedMessage(&b, m.ReferencedMessage, app.Session.State.User.ID) + buildReferencedMessage(&b, m.ReferencedMessage, app.State.Ready().User.ID) if app.Config.Timestamps { loc, err := time.LoadLocation(app.Config.Timezone) @@ -30,18 +29,18 @@ func buildMessage(app *App, m *astatine.Message) []byte { } b.WriteString("[::d]") - b.WriteString(m.Timestamp.In(loc).Format(time.Stamp)) + b.WriteString(m.Timestamp.Time().In(loc).Format(time.Stamp)) b.WriteString("[::-]") b.WriteByte(' ') } // Build the author of this message. - buildAuthor(&b, m.Author, app.Session.State.User.ID) + buildAuthor(&b, m.Author, app.State.Ready().User.ID) // Build the contents of the message. - buildContent(&b, m, app.Session.State.User.ID) + buildContent(&b, m, app.State.Ready().User.ID) - if m.EditedTimestamp != nil { + if m.EditedTimestamp.IsValid() { b.WriteString(" [::d](edited)[::-]") } @@ -56,19 +55,19 @@ func buildMessage(app *App, m *astatine.Message) []byte { b.WriteString("[\"\"]") b.WriteByte('\n') - case astatine.MessageTypeGuildMemberJoin: + case discord.GuildMemberJoinMessage: b.WriteString("[#5865F2]") b.WriteString(m.Author.Username) b.WriteString("[-] joined the server.") b.WriteByte('\n') - case astatine.MessageTypeCall: + case discord.CallMessage: b.WriteString("[#5865F2]") b.WriteString(m.Author.Username) b.WriteString("[-] started a call.") b.WriteByte('\n') - case astatine.MessageTypeChannelPinnedMessage: + case discord.ChannelPinnedMessage: b.WriteString("[#5865F2]") b.WriteString(m.Author.Username) b.WriteString("[-] pinned a message.") @@ -86,7 +85,7 @@ func buildMessage(app *App, m *astatine.Message) []byte { return nil } -func buildReferencedMessage(b *strings.Builder, rm *astatine.Message, clientID string) { +func buildReferencedMessage(b *strings.Builder, rm *discord.Message, clientID discord.UserID) { if rm != nil { b.WriteString(" ╭ ") b.WriteString("[::d]") @@ -94,7 +93,7 @@ func buildReferencedMessage(b *strings.Builder, rm *astatine.Message, clientID s if rm.Content != "" { rm.Content = buildMentions(rm.Content, rm.Mentions, clientID) - b.WriteString(discord.ParseMarkdown(rm.Content)) + b.WriteString(parseMarkdown(rm.Content)) } b.WriteString("[::-]") @@ -102,16 +101,16 @@ func buildReferencedMessage(b *strings.Builder, rm *astatine.Message, clientID s } } -func buildContent(b *strings.Builder, m *astatine.Message, clientID string) { +func buildContent(b *strings.Builder, m discord.Message, clientID discord.UserID) { if m.Content != "" { m.Content = buildMentions(m.Content, m.Mentions, clientID) - b.WriteString(discord.ParseMarkdown(m.Content)) + b.WriteString(parseMarkdown(m.Content)) } } -func buildEmbeds(b *strings.Builder, es []*astatine.MessageEmbed) { +func buildEmbeds(b *strings.Builder, es []discord.Embed) { for _, e := range es { - if e.Type != astatine.EmbedTypeRich { + if e.Type != discord.NormalEmbed { continue } @@ -148,7 +147,7 @@ func buildEmbeds(b *strings.Builder, es []*astatine.MessageEmbed) { embedBuilder.WriteByte('\n') } - embedBuilder.WriteString(discord.ParseMarkdown(e.Description)) + embedBuilder.WriteString(parseMarkdown(e.Description)) } if len(e.Fields) != 0 { @@ -162,7 +161,7 @@ func buildEmbeds(b *strings.Builder, es []*astatine.MessageEmbed) { embedBuilder.WriteString(ef.Name) embedBuilder.WriteString("[::-]") embedBuilder.WriteByte('\n') - embedBuilder.WriteString(discord.ParseMarkdown(ef.Value)) + embedBuilder.WriteString(parseMarkdown(ef.Value)) if i != len(e.Fields)-1 { embedBuilder.WriteString("\n\n") @@ -182,7 +181,7 @@ func buildEmbeds(b *strings.Builder, es []*astatine.MessageEmbed) { } } -func buildAttachments(b *strings.Builder, as []*astatine.MessageAttachment) { +func buildAttachments(b *strings.Builder, as []discord.Attachment) { for _, a := range as { b.WriteByte('\n') b.WriteByte('[') @@ -192,7 +191,7 @@ func buildAttachments(b *strings.Builder, as []*astatine.MessageAttachment) { } } -func buildMentions(content string, mentions []*astatine.User, clientID string) string { +func buildMentions(content string, mentions []discord.GuildUser, clientID discord.UserID) string { for _, mUser := range mentions { var color string if mUser.ID == clientID { @@ -203,10 +202,10 @@ func buildMentions(content string, mentions []*astatine.User, clientID string) s content = strings.NewReplacer( // <@USER_ID> - "<@"+mUser.ID+">", + "<@"+mUser.ID.String()+">", color+"@"+mUser.Username+"[-:-]", // <@!USER_ID> - "<@!"+mUser.ID+">", + "<@!"+mUser.ID.String()+">", color+"@"+mUser.Username+"[-:-]", ).Replace(content) } @@ -214,7 +213,7 @@ func buildMentions(content string, mentions []*astatine.User, clientID string) s return content } -func buildAuthor(b *strings.Builder, u *astatine.User, clientID string) { +func buildAuthor(b *strings.Builder, u discord.User, clientID discord.UserID) { if u.ID == clientID { b.WriteString("[#57F287]") } else { diff --git a/ui/channels.go b/ui/channels.go index b7c7279..fc63817 100644 --- a/ui/channels.go +++ b/ui/channels.go @@ -1,77 +1,77 @@ package ui import ( - "github.com/ayntgl/astatine" - "github.com/ayntgl/discordo/discord" + "github.com/diamondburned/arikawa/v3/discord" "github.com/rivo/tview" ) -type ChannelsTreeView struct { +type ChannelsTree struct { *tview.TreeView app *App } -func NewChannelsTreeView(app *App) *ChannelsTreeView { - ctv := &ChannelsTreeView{ +func NewChannelsTree(app *App) *ChannelsTree { + ct := &ChannelsTree{ TreeView: tview.NewTreeView(), app: app, } - ctv.SetTopLevel(1) - ctv.SetRoot(tview.NewTreeNode("")) - ctv.SetTitle("Channels") - ctv.SetTitleAlign(tview.AlignLeft) - ctv.SetBorder(true) - ctv.SetBorderPadding(0, 0, 1, 1) - ctv.SetSelectedFunc(ctv.onSelected) - return ctv + ct.SetRoot(tview.NewTreeNode("")) + ct.SetTopLevel(1) + ct.SetSelectedFunc(ct.onSelected) + + ct.SetTitle("Channels") + ct.SetTitleAlign(tview.AlignLeft) + ct.SetBorder(true) + ct.SetBorderPadding(0, 0, 1, 1) + + return ct } -func (ctv *ChannelsTreeView) onSelected(n *tview.TreeNode) { - ctv.app.SelectedMessage = -1 - ctv.app.MessagesTextView. +func (ct *ChannelsTree) onSelected(node *tview.TreeNode) { + ct.app.SelectedChannel = nil + ct.app.SelectedMessage = -1 + ct.app.MessagesTextView. Highlight(). Clear(). SetTitle("") - ctv.app.MessageInputField.SetText("") + ct.app.MessageInputField.SetText("") - c, err := ctv.app.Session.State.Channel(n.GetReference().(string)) + ref := node.GetReference() + c, err := ct.app.State.Cabinet.Channel(ref.(discord.ChannelID)) if err != nil { return } - if c.Type == astatine.ChannelTypeGuildCategory { - n.SetExpanded(!n.IsExpanded()) + // If the channel is a category channel, expend the selected node if it is not expanded already. + if c.Type == discord.GuildCategory { + node.SetExpanded(!node.IsExpanded()) return } - ctv.app.SelectedChannel = c - ctv.app.SetFocus(ctv.app.MessageInputField) + ct.app.SelectedChannel = c + ct.app.SetFocus(ct.app.MessageInputField) - title := discord.ChannelToString(c) + title := channelToString(*c) if c.Topic != "" { - title += " - " + discord.ParseMarkdown(c.Topic) + title += " - " + parseMarkdown(c.Topic) } - ctv.app.MessagesTextView.SetTitle(title) + ct.app.MessagesTextView.SetTitle(title) go func() { - ms, err := ctv.app.Session.ChannelMessages(c.ID, ctv.app.Config.MessagesLimit, "", "", "") + // The returned slice will be sorted from latest to oldest. + ms, err := ct.app.State.Messages(c.ID, ct.app.Config.MessagesLimit) if err != nil { return } for i := len(ms) - 1; i >= 0; i-- { - ctv.app.SelectedChannel.Messages = append(ctv.app.SelectedChannel.Messages, ms[i]) - ctv.drawMessage(ms[i]) + _, err = ct.app.MessagesTextView.Write(buildMessage(ct.app, ms[i])) + if err != nil { + return + } } - ctv.app.MessagesTextView.ScrollToEnd() + ct.app.MessagesTextView.ScrollToEnd() }() } - -func (ctv *ChannelsTreeView) drawMessage(m *astatine.Message) { - _, err := ctv.app.MessagesTextView.Write(buildMessage(ctv.app, m)) - if err != nil { - return - } -} diff --git a/ui/guilds.go b/ui/guilds.go index 9db29da..98e26de 100644 --- a/ui/guilds.go +++ b/ui/guilds.go @@ -3,129 +3,121 @@ package ui import ( "sort" - "github.com/ayntgl/astatine" - "github.com/ayntgl/discordo/discord" - "github.com/gdamore/tcell/v2" + dsc "github.com/diamondburned/arikawa/v3/discord" "github.com/rivo/tview" ) -type GuildsList struct { - *tview.List +type GuildsTree struct { + *tview.TreeView app *App } -func NewGuildsList(app *App) *GuildsList { - gl := &GuildsList{ - List: tview.NewList(), - app: app, +func NewGuildsTree(app *App) *GuildsTree { + gt := &GuildsTree{ + TreeView: tview.NewTreeView(), + app: app, } - gl.AddItem("Direct Messages", "", 0, nil) - gl.ShowSecondaryText(false) - gl.SetTitle("Guilds") - gl.SetTitleAlign(tview.AlignLeft) - gl.SetBorder(true) - gl.SetBorderPadding(0, 0, 1, 1) - gl.SetSelectedFunc(gl.onSelected) - gl.SetInputCapture(gl.onInputCapture) - return gl + rootNode := tview.NewTreeNode("") + rootNode.AddChild(tview.NewTreeNode("Direct Messages")) + + gt.SetRoot(rootNode) + gt.SetTopLevel(1) + gt.SetSelectedFunc(gt.onSelected) + + gt.SetTitle("Guilds") + gt.SetTitleAlign(tview.AlignLeft) + gt.SetBorder(true) + gt.SetBorderPadding(0, 0, 1, 1) + + return gt } -func (gl *GuildsList) onInputCapture(e *tcell.EventKey) *tcell.EventKey { - i := gl.List.GetCurrentItem() - switch e.Rune() { - case 'g': // Home. - i = 0 - case 'G': // End. - i = gl.List.GetItemCount() - 1 - case 'j': // Down. - i++ - case 'k': // Up. - i-- - if i < 0 { - i = 0 - } - default: - return e - } - gl.List.SetCurrentItem(i) - return nil -} - -func (gl *GuildsList) onSelected(idx int, mainText string, secondaryText string, shortcut rune) { - rootTreeNode := gl.app.ChannelsTreeView.GetRoot() - rootTreeNode.ClearChildren() - gl.app.SelectedMessage = -1 - gl.app.MessagesTextView. +func (gt *GuildsTree) onSelected(node *tview.TreeNode) { + gt.app.SelectedChannel = nil + gt.app.SelectedMessage = -1 + rootNode := gt.app.ChannelsTree.GetRoot() + rootNode.ClearChildren() + gt.app.MessagesTextView. Highlight(). - Clear() - gl.app.MessageInputField.SetText("") + Clear(). + SetTitle("") + gt.app.MessageInputField.SetText("") + + ref := node.GetReference() + // If the reference of the selected node is nil, it must be the direct messages node. + if ref == nil { + cs, err := gt.app.State.Cabinet.PrivateChannels() + if err != nil { + return + } - if mainText == "Direct Messages" { - cs := gl.app.Session.State.PrivateChannels sort.Slice(cs, func(i, j int) bool { return cs[i].LastMessageID > cs[j].LastMessageID }) for _, c := range cs { - channelTreeNode := tview.NewTreeNode(discord.ChannelToString(c)). - SetReference(c.ID) - rootTreeNode.AddChild(channelTreeNode) + channelNode := tview.NewTreeNode(c.Name) + channelNode.SetReference(c.ID) + rootNode.AddChild(channelNode) } } else { // Guild - // Decrement the index of the selected item by one since the first item in the list is always "Direct Messages". - cs := gl.app.Session.State.Guilds[idx-1].Channels + cs, err := gt.app.State.Cabinet.Channels(ref.(dsc.GuildID)) + if err != nil { + return + } + sort.Slice(cs, func(i, j int) bool { return cs[i].Position < cs[j].Position }) for _, c := range cs { - if (c.Type == astatine.ChannelTypeGuildText || c.Type == astatine.ChannelTypeGuildNews) && (c.ParentID == "") { - channelTreeNode := tview.NewTreeNode(discord.ChannelToString(c)). - SetReference(c.ID) - rootTreeNode.AddChild(channelTreeNode) + if (c.Type == dsc.GuildText || c.Type == dsc.GuildNews) && (!c.ParentID.IsValid()) { + channelNode := tview.NewTreeNode(channelToString(c)) + channelNode.SetReference(c.ID) + rootNode.AddChild(channelNode) } } CATEGORY: for _, c := range cs { - if c.Type == astatine.ChannelTypeGuildCategory { + if c.Type == dsc.GuildCategory { for _, nestedChannel := range cs { if nestedChannel.ParentID == c.ID { - channelTreeNode := tview.NewTreeNode(c.Name). - SetReference(c.ID) - rootTreeNode.AddChild(channelTreeNode) + channelNode := tview.NewTreeNode(c.Name) + channelNode.SetReference(c.ID) + rootNode.AddChild(channelNode) continue CATEGORY } } - channelTreeNode := tview.NewTreeNode(c.Name). - SetReference(c.ID) - rootTreeNode.AddChild(channelTreeNode) + channelNode := tview.NewTreeNode(channelToString(c)) + channelNode.SetReference(c.ID) + rootNode.AddChild(channelNode) } } for _, c := range cs { - if (c.Type == astatine.ChannelTypeGuildText || c.Type == astatine.ChannelTypeGuildNews) && (c.ParentID != "") { - var parentTreeNode *tview.TreeNode - rootTreeNode.Walk(func(node, _ *tview.TreeNode) bool { + if (c.Type == dsc.GuildText || c.Type == dsc.GuildNews) && (c.ParentID.IsValid()) { + var parentNode *tview.TreeNode + rootNode.Walk(func(node, _ *tview.TreeNode) bool { if node.GetReference() == c.ParentID { - parentTreeNode = node + parentNode = node return false } return true }) - if parentTreeNode != nil { - channelTreeNode := tview.NewTreeNode(discord.ChannelToString(c)). - SetReference(c.ID) - parentTreeNode.AddChild(channelTreeNode) + if parentNode != nil { + channelNode := tview.NewTreeNode(channelToString(c)) + channelNode.SetReference(c.ID) + parentNode.AddChild(channelNode) } } } } - gl.app.ChannelsTreeView.SetCurrentNode(rootTreeNode) - gl.app.SetFocus(gl.app.ChannelsTreeView) + gt.app.ChannelsTree.SetCurrentNode(rootNode) + gt.app.SetFocus(gt.app.ChannelsTree) } diff --git a/discord/markdown.go b/ui/markdown.go similarity index 90% rename from discord/markdown.go rename to ui/markdown.go index b77d597..d698f42 100644 --- a/discord/markdown.go +++ b/ui/markdown.go @@ -1,4 +1,4 @@ -package discord +package ui import "regexp" @@ -9,7 +9,7 @@ var ( strikeThroughRegex = regexp.MustCompile(`(?ms)~~(.*?)~~`) ) -func ParseMarkdown(md string) string { +func parseMarkdown(md string) string { var res string res = boldRegex.ReplaceAllString(md, "[::b]$1[::-]") res = italicRegex.ReplaceAllString(res, "[::i]$1[::-]") diff --git a/ui/messages.go b/ui/messages.go index b1013db..dd14d16 100644 --- a/ui/messages.go +++ b/ui/messages.go @@ -2,7 +2,6 @@ package ui import ( "io" - "io/ioutil" "net/http" "os" "os/exec" @@ -11,8 +10,9 @@ import ( "strings" "github.com/atotto/clipboard" - "github.com/ayntgl/astatine" - "github.com/ayntgl/discordo/discord" + "github.com/diamondburned/arikawa/v3/api" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/utils/json/option" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "github.com/skratchdot/open-golang/open" @@ -34,13 +34,15 @@ func NewMessagesTextView(app *App) *MessagesTextView { mtv.SetDynamicColors(true) mtv.SetRegions(true) mtv.SetWordWrap(true) + mtv.SetInputCapture(mtv.onInputCapture) mtv.SetChangedFunc(func() { mtv.app.Draw() }) + mtv.SetTitleAlign(tview.AlignLeft) mtv.SetBorder(true) mtv.SetBorderPadding(0, 0, 1, 1) - mtv.SetInputCapture(mtv.onInputCapture) + return mtv } @@ -49,50 +51,57 @@ func (mtv *MessagesTextView) onInputCapture(e *tcell.EventKey) *tcell.EventKey { return nil } - ms := mtv.app.SelectedChannel.Messages - if len(ms) == 0 { + // Messages should return messages ordered from latest to earliest. + ms, err := mtv.app.State.Cabinet.Messages(mtv.app.SelectedChannel.ID) + if err != nil || len(ms) == 0 { return nil } switch e.Name() { case mtv.app.Config.Keys.SelectPreviousMessage: + // If there are no highlighted regions, select the latest (last) message in the messages TextView. if len(mtv.app.MessagesTextView.GetHighlights()) == 0 { - mtv.app.SelectedMessage = len(ms) - 1 + mtv.app.SelectedMessage = 0 } else { - mtv.app.SelectedMessage-- - if mtv.app.SelectedMessage < 0 { + // If the selected message is the oldest (first) message, select the latest (last) message in the messages TextView. + if mtv.app.SelectedMessage == len(ms)-1 { mtv.app.SelectedMessage = 0 + } else { + mtv.app.SelectedMessage++ } } mtv.app.MessagesTextView. - Highlight(ms[mtv.app.SelectedMessage].ID). + Highlight(ms[mtv.app.SelectedMessage].ID.String()). ScrollToHighlight() return nil case mtv.app.Config.Keys.SelectNextMessage: + // If there are no highlighted regions, select the latest (last) message in the messages TextView. if len(mtv.app.MessagesTextView.GetHighlights()) == 0 { - mtv.app.SelectedMessage = len(ms) - 1 + mtv.app.SelectedMessage = 0 } else { - mtv.app.SelectedMessage++ - if mtv.app.SelectedMessage >= len(ms) { + // If the selected message is the latest (last) message, select the oldest (first) message in the messages TextView. + if mtv.app.SelectedMessage == 0 { mtv.app.SelectedMessage = len(ms) - 1 + } else { + mtv.app.SelectedMessage-- } } mtv.app.MessagesTextView. - Highlight(ms[mtv.app.SelectedMessage].ID). + Highlight(ms[mtv.app.SelectedMessage].ID.String()). ScrollToHighlight() return nil case mtv.app.Config.Keys.SelectFirstMessage: - mtv.app.SelectedMessage = 0 + mtv.app.SelectedMessage = len(ms) - 1 mtv.app.MessagesTextView. - Highlight(ms[mtv.app.SelectedMessage].ID). + Highlight(ms[mtv.app.SelectedMessage].ID.String()). ScrollToHighlight() return nil case mtv.app.Config.Keys.SelectLastMessage: - mtv.app.SelectedMessage = len(ms) - 1 + mtv.app.SelectedMessage = 0 mtv.app.MessagesTextView. - Highlight(ms[mtv.app.SelectedMessage].ID). + Highlight(ms[mtv.app.SelectedMessage].ID.String()). ScrollToHighlight() return nil case mtv.app.Config.Keys.OpenMessageActionsList: @@ -101,7 +110,12 @@ func (mtv *MessagesTextView) onInputCapture(e *tcell.EventKey) *tcell.EventKey { return nil } - _, m := discord.FindMessageByID(mtv.app.SelectedChannel.Messages, hs[0]) + mID, err := discord.ParseSnowflake(hs[0]) + if err != nil { + return nil + } + + _, m := findMessageByID(ms, discord.MessageID(mID)) if m == nil { return nil } @@ -119,16 +133,16 @@ func (mtv *MessagesTextView) onInputCapture(e *tcell.EventKey) *tcell.EventKey { actionsList.SetBorderPadding(0, 0, 1, 1) // If the client user has `SEND_MESSAGES` permission, add a new action to reply to the message. - if discord.HasPermission(mtv.app.Session.State, mtv.app.SelectedChannel.ID, astatine.PermissionSendMessages) { + if hasPermission(mtv.app.State, mtv.app.SelectedChannel.ID, discord.PermissionSendMessages) { actionsList.AddItem("Reply", "", 'r', func() { - mtv.app.MessageInputField.SetTitle("Replying to " + m.Author.String()) + mtv.app.MessageInputField.SetTitle("Replying to " + m.Author.Tag()) mtv.app. SetRoot(mtv.app.MainFlex, true). SetFocus(mtv.app.MessageInputField) }) actionsList.AddItem("Mention Reply", "", 'R', func() { - mtv.app.MessageInputField.SetTitle("[@] Replying to " + m.Author.String()) + mtv.app.MessageInputField.SetTitle("[@] Replying to " + m.Author.Tag()) mtv.app. SetRoot(mtv.app.MainFlex, true). SetFocus(mtv.app.MessageInputField) @@ -136,9 +150,9 @@ func (mtv *MessagesTextView) onInputCapture(e *tcell.EventKey) *tcell.EventKey { } // If the client user has the `MANAGE_MESSAGES` permission, add a new action to delete the message. - if discord.HasPermission(mtv.app.Session.State, mtv.app.SelectedChannel.ID, astatine.PermissionManageMessages) { + if hasPermission(mtv.app.State, mtv.app.SelectedChannel.ID, discord.PermissionManageMessages) { actionsList.AddItem("Delete", "", 'd', func() { - go mtv.deleteMessage(m) + go mtv.deleteMessage(*m) mtv.app. SetRoot(mtv.app.MainFlex, true). SetFocus(mtv.app.MessagesTextView) @@ -148,9 +162,9 @@ func (mtv *MessagesTextView) onInputCapture(e *tcell.EventKey) *tcell.EventKey { // If the referenced message exists, add a new action to select the reply. if m.ReferencedMessage != nil { actionsList.AddItem("Select Reply", "", 'm', func() { - mtv.app.SelectedMessage, _ = discord.FindMessageByID(mtv.app.SelectedChannel.Messages, m.ReferencedMessage.ID) + mtv.app.SelectedMessage, _ = findMessageByID(ms, m.ReferencedMessage.ID) mtv.app.MessagesTextView. - Highlight(m.ReferencedMessage.ID). + Highlight(m.ReferencedMessage.ID.String()). ScrollToHighlight() mtv.app. SetRoot(mtv.app.MainFlex, true). @@ -189,7 +203,7 @@ func (mtv *MessagesTextView) onInputCapture(e *tcell.EventKey) *tcell.EventKey { mtv.app.SetFocus(mtv.app.MessagesTextView) }) actionsList.AddItem("Copy ID", "", 'i', func() { - if err := clipboard.WriteAll(m.ID); err != nil { + if err := clipboard.WriteAll(m.ID.String()); err != nil { return } @@ -211,7 +225,7 @@ func (mtv *MessagesTextView) onInputCapture(e *tcell.EventKey) *tcell.EventKey { return e } -func (mtv *MessagesTextView) downloadAttachment(as []*astatine.MessageAttachment) error { +func (mtv *MessagesTextView) downloadAttachment(as []discord.Attachment) error { for _, a := range as { f, err := os.Create(filepath.Join(mtv.app.Config.AttachmentDownloadsDir, a.Filename)) if err != nil { @@ -224,7 +238,7 @@ func (mtv *MessagesTextView) downloadAttachment(as []*astatine.MessageAttachment return err } - d, err := ioutil.ReadAll(resp.Body) + d, err := io.ReadAll(resp.Body) if err != nil { return err } @@ -235,7 +249,7 @@ func (mtv *MessagesTextView) downloadAttachment(as []*astatine.MessageAttachment return nil } -func (mtv *MessagesTextView) openAttachment(as []*astatine.MessageAttachment) error { +func (mtv *MessagesTextView) openAttachment(as []discord.Attachment) error { for _, a := range as { cacheDirPath, _ := os.UserCacheDir() f, err := os.Create(filepath.Join(cacheDirPath, a.Filename)) @@ -249,7 +263,7 @@ func (mtv *MessagesTextView) openAttachment(as []*astatine.MessageAttachment) er return err } - d, err := ioutil.ReadAll(resp.Body) + d, err := io.ReadAll(resp.Body) if err != nil { return err } @@ -261,28 +275,40 @@ func (mtv *MessagesTextView) openAttachment(as []*astatine.MessageAttachment) er return nil } -func (mtv *MessagesTextView) deleteMessage(m *astatine.Message) { +func (mtv *MessagesTextView) deleteMessage(m discord.Message) { mtv.Clear() - mtv.app.SelectedChannel.Messages = append(mtv.app.SelectedChannel.Messages[:mtv.app.SelectedMessage], mtv.app.SelectedChannel.Messages[mtv.app.SelectedMessage+1:]...) - - err := mtv.app.Session.ChannelMessageDelete(m.ChannelID, m.ID) + err := mtv.app.State.MessageRemove(m.ChannelID, m.ID) if err != nil { return } - for _, m := range mtv.app.SelectedChannel.Messages { - mtv.app.ChannelsTreeView.drawMessage(m) + err = mtv.app.State.DeleteMessage(m.ChannelID, m.ID, "Unknown") + if err != nil { + return + } + + // The returned slice will be sorted from latest to oldest. + ms, err := mtv.app.State.Messages(m.ChannelID, mtv.app.Config.MessagesLimit) + if err != nil { + return + } + + for i := len(ms) - 1; i >= 0; i-- { + _, err = mtv.app.MessagesTextView.Write(buildMessage(mtv.app, ms[i])) + if err != nil { + return + } } } -type MessageInputField struct { +type MessageInput struct { *tview.InputField app *App } -func NewMessageInputField(app *App) *MessageInputField { - mi := &MessageInputField{ +func NewMessageInput(app *App) *MessageInput { + mi := &MessageInput{ InputField: tview.NewInputField(), app: app, } @@ -297,7 +323,7 @@ func NewMessageInputField(app *App) *MessageInputField { return mi } -func (mi *MessageInputField) onInputCapture(e *tcell.EventKey) *tcell.EventKey { +func (mi *MessageInput) onInputCapture(e *tcell.EventKey) *tcell.EventKey { switch e.Name() { case "Enter": if mi.app.SelectedChannel == nil { @@ -309,27 +335,37 @@ func (mi *MessageInputField) onInputCapture(e *tcell.EventKey) *tcell.EventKey { return nil } + ms, err := mi.app.State.Messages(mi.app.SelectedChannel.ID, mi.app.Config.MessagesLimit) + if err != nil { + return nil + } + if len(mi.app.MessagesTextView.GetHighlights()) != 0 { - _, m := discord.FindMessageByID(mi.app.SelectedChannel.Messages, mi.app.MessagesTextView.GetHighlights()[0]) - d := &astatine.MessageSend{ - Content: t, - Reference: m.Reference(), - AllowedMentions: &astatine.MessageAllowedMentions{RepliedUser: false}, - } - if strings.HasPrefix(mi.app.MessageInputField.GetTitle(), "[@]") { - d.AllowedMentions.RepliedUser = true - } else { - d.AllowedMentions.RepliedUser = false + mID, err := discord.ParseSnowflake(mi.app.MessagesTextView.GetHighlights()[0]) + if err != nil { + return nil } - go mi.app.Session.ChannelMessageSendComplex(m.ChannelID, d) + _, m := findMessageByID(ms, discord.MessageID(mID)) + d := api.SendMessageData{ + Content: t, + Reference: m.Reference, + AllowedMentions: &api.AllowedMentions{RepliedUser: option.False}, + } + + // If the title of the message InputField widget has "[@]" as a prefix, send the message as a reply and mention the replied user. + if strings.HasPrefix(mi.app.MessageInputField.GetTitle(), "[@]") { + d.AllowedMentions.RepliedUser = option.True + } + + go mi.app.State.SendMessageComplex(m.ChannelID, d) mi.app.SelectedMessage = -1 mi.app.MessagesTextView.Highlight() mi.app.MessageInputField.SetTitle("") } else { - go mi.app.Session.ChannelMessageSend(mi.app.SelectedChannel.ID, t) + go mi.app.State.SendMessage(mi.app.SelectedChannel.ID, t) } mi.app.MessageInputField.SetText("") diff --git a/ui/util.go b/ui/util.go new file mode 100644 index 0000000..37ed281 --- /dev/null +++ b/ui/util.go @@ -0,0 +1,46 @@ +package ui + +import ( + "strings" + + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/state" +) + +func channelToString(c discord.Channel) string { + var repr string + if c.Name != "" { + repr = "#" + c.Name + } else if len(c.DMRecipients) == 1 { + rp := c.DMRecipients[0] + repr = rp.Username + "#" + rp.Discriminator + } else { + rps := make([]string, len(c.DMRecipients)) + for i, r := range c.DMRecipients { + rps[i] = r.Username + "#" + r.Discriminator + } + + repr = strings.Join(rps, ", ") + } + + return repr +} + +func findMessageByID(ms []discord.Message, mID discord.MessageID) (int, *discord.Message) { + for i, m := range ms { + if m.ID == mID { + return i, &m + } + } + + return -1, nil +} + +func hasPermission(s *state.State, cID discord.ChannelID, p discord.Permissions) bool { + perm, err := s.Permissions(cID, s.Ready().User.ID) + if err != nil { + return false + } + + return perm&p == p +}