diff --git a/category.go b/category.go new file mode 100644 index 0000000..3615b66 --- /dev/null +++ b/category.go @@ -0,0 +1,67 @@ +package discord + +import ( + "sort" + + "github.com/diamondburned/arikawa/discord" + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat/text" + "github.com/pkg/errors" +) + +type Category struct { + id discord.Snowflake + guildID discord.Snowflake + session *Session +} + +var ( + _ cchat.Server = (*Category)(nil) + _ cchat.ServerList = (*Category)(nil) +) + +func NewCategory(s *Session, ch discord.Channel) *Category { + return &Category{ + id: ch.ID, + guildID: ch.GuildID, + session: s, + } +} + +func (c *Category) ID() string { + return c.id.String() +} + +func (c *Category) Name() text.Rich { + t, err := c.session.Channel(c.id) + if err != nil { + // This shouldn't happen. + return text.Rich{Content: c.id.String()} + } + + return text.Rich{ + Content: t.Name, + } +} + +func (c *Category) Servers(container cchat.ServersContainer) error { + t, err := c.session.Channels(c.guildID) + if err != nil { + return errors.Wrap(err, "Failed to get channels") + } + + // Filter out channels with this category ID. + var chs = filterAccessible(c.session, filterCategory(t, c.id)) + + sort.Slice(chs, func(i, j int) bool { + return chs[i].Position < chs[j].Position + }) + + var chv = make([]cchat.Server, len(chs)) + for i := range chs { + chv[i] = NewChannel(c.session, chs[i]) + } + + container.SetServers(chv) + return nil +} diff --git a/channel.go b/channel.go index 38ab823..b9fe61f 100644 --- a/channel.go +++ b/channel.go @@ -12,6 +12,50 @@ import ( "github.com/pkg/errors" ) +func chGuildCheck(chType discord.ChannelType) bool { + switch chType { + case discord.GuildCategory, discord.GuildText: + return true + default: + return false + } +} + +func filterAccessible(s *Session, chs []discord.Channel) []discord.Channel { + u, err := s.Me() + if err != nil { + // Shouldn't happen. + return chs + } + + filtered := chs[:0] + + for _, ch := range chs { + p, err := s.Permissions(ch.ID, u.ID) + if err != nil { + continue + } + + if p.Has(discord.PermissionViewChannel) { + filtered = append(filtered, ch) + } + } + + return filtered +} + +func filterCategory(chs []discord.Channel, catID discord.Snowflake) []discord.Channel { + var filtered = chs[:0] + + for _, ch := range chs { + if ch.CategoryID == catID && chGuildCheck(ch.Type) { + filtered = append(filtered, ch) + } + } + + return filtered +} + type Channel struct { id discord.Snowflake guildID discord.Snowflake diff --git a/go.mod b/go.mod index 0385517..6f07169 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,11 @@ module github.com/diamondburned/cchat-discord go 1.14 -replace github.com/diamondburned/ningen => ../../ningen/ - require ( github.com/davecgh/go-spew v1.1.1 - github.com/diamondburned/arikawa v0.9.4 + github.com/diamondburned/arikawa v0.9.5 github.com/diamondburned/cchat v0.0.31 - github.com/diamondburned/ningen v0.0.0-20200618230530-16d4d7fbc521 + github.com/diamondburned/ningen v0.1.0 github.com/go-test/deep v1.0.6 github.com/pkg/errors v0.9.1 github.com/yuin/goldmark v1.1.30 diff --git a/go.sum b/go.sum index f5c20d5..11b7de0 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,10 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/diamondburned/arikawa v0.8.7-0.20200522214036-530bff74a2c6/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= github.com/diamondburned/arikawa v0.9.4 h1:Mrp0Vz9R2afbvhWS6m/oLIQy22/uxXb459LUv7qrZPA= github.com/diamondburned/arikawa v0.9.4/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= +github.com/diamondburned/arikawa v0.9.5-0.20200619075944-01021f09025b h1:fTR1esGjH+rR3ssd2vlZoxYLtML29paO0JjRV2lB3s0= +github.com/diamondburned/arikawa v0.9.5-0.20200619075944-01021f09025b/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= +github.com/diamondburned/arikawa v0.9.5 h1:P1ffsp+NHT22wWKYFVC8CdlGRLzPuUV9FcCBKOCJpCI= +github.com/diamondburned/arikawa v0.9.5/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= github.com/diamondburned/cchat v0.0.26 h1:QBt4d65uzUPJz3jF8b2pJ09Jz8LeBRyG2ol47FOy0g0= github.com/diamondburned/cchat v0.0.26/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/cchat v0.0.28 h1:+1VnltW0rl8/NZTUP+x89jVhi3YTTR+e6iLprZ7HcwM= @@ -11,6 +15,10 @@ github.com/diamondburned/cchat v0.0.31 h1:yUgrh5xbGX0R55glyxYtVewIDL2eXLJ+okIEfV github.com/diamondburned/cchat v0.0.31/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/ningen v0.0.0-20200610212436-159f7105a2be h1:mUw8X/YzJGFSdL8y3Q/XqyzqPyIMNVSDyZGOP3JXgJA= github.com/diamondburned/ningen v0.0.0-20200610212436-159f7105a2be/go.mod h1:B2hq2B4va1MlnMmXuv9vXmyu9gscxJLmwrmcSB1Les8= +github.com/diamondburned/ningen v0.0.0-20200619080103-03201e09d2a9 h1:KmmKgOVCXiRufPhBWlGpSILM6ZAUvgATMelQfYNpZIw= +github.com/diamondburned/ningen v0.0.0-20200619080103-03201e09d2a9/go.mod h1:Oxl8JrIoyo/OTx2xq9DnvvkJ3V3fUeI6cftO4LmNX84= +github.com/diamondburned/ningen v0.1.0 h1:cTnRNrN0g2Wr/kgjLLpa3pqlbEd6JPNa1yGDer8uV4U= +github.com/diamondburned/ningen v0.1.0/go.mod h1:1vi8mlBlM2xjJy+IugU51q+IMgyNXggS4Xv3SPFd2Q4= github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8= github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= diff --git a/guild.go b/guild.go index 5053719..5af4282 100644 --- a/guild.go +++ b/guild.go @@ -2,13 +2,83 @@ package discord import ( "context" + "sort" + "strings" "github.com/diamondburned/arikawa/discord" + "github.com/diamondburned/arikawa/gateway" "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/segments" "github.com/diamondburned/cchat/text" "github.com/pkg/errors" ) +type GuildFolder struct { + gateway.GuildFolder + session *Session +} + +var ( + _ cchat.Server = (*Guild)(nil) + _ cchat.ServerList = (*Guild)(nil) +) + +func NewGuildFolder(s *Session, gf gateway.GuildFolder) *GuildFolder { + // Name should never be empty. + if gf.Name == "" { + var names = make([]string, 0, len(gf.GuildIDs)) + + for _, id := range gf.GuildIDs { + if g, _ := s.Store.Guild(id); g != nil { + names = append(names, g.Name) + } + } + + gf.Name = strings.Join(names, ", ") + } + + return &GuildFolder{ + GuildFolder: gf, + session: s, + } +} + +func (gf *GuildFolder) ID() string { + return gf.GuildFolder.ID.String() +} + +func (gf *GuildFolder) Name() text.Rich { + var name = text.Rich{ + // 1en space for style. + Content: gf.GuildFolder.Name, + } + + if gf.GuildFolder.Color > 0 { + name.Segments = []text.Segment{ + // The length of this black box is actually 3. Mind == blown. + segments.NewColored(len(name.Content), gf.GuildFolder.Color.Uint32()), + } + } + + return name +} + +func (gf *GuildFolder) Servers(container cchat.ServersContainer) error { + var servers = make([]cchat.Server, len(gf.GuildIDs)) + + for i, id := range gf.GuildIDs { + g, err := gf.session.Guild(id) + if err != nil { + return errors.Wrap(err, "Failed to get guild ID "+id.String()) + } + + servers[i] = NewGuild(gf.session, g) + } + + container.SetServers(servers) + return nil +} + type Guild struct { id discord.Snowflake session *Session @@ -27,6 +97,15 @@ func NewGuild(s *Session, g *discord.Guild) *Guild { } } +func NewGuildFromID(s *Session, gID discord.Snowflake) (*Guild, error) { + g, err := s.Guild(gID) + if err != nil { + return nil, err + } + + return NewGuild(s, g), nil +} + func (g *Guild) self(ctx context.Context) (*discord.Guild, error) { return g.session.WithContext(ctx).Guild(g.id) } @@ -68,11 +147,24 @@ func (g *Guild) Servers(container cchat.ServersContainer) error { return errors.Wrap(err, "Failed to get channels") } - var channels = make([]cchat.Server, len(c)) - for i := range c { - channels[i] = NewChannel(g.session, c[i]) + // Only get top-level channels (those with category ID being null). + var toplevels = filterAccessible(g.session, filterCategory(c, discord.NullSnowflake)) + + sort.Slice(toplevels, func(i, j int) bool { + return toplevels[i].Position < toplevels[j].Position + }) + + var chs = make([]cchat.Server, 0, len(toplevels)) + + for _, ch := range toplevels { + switch ch.Type { + case discord.GuildCategory: + chs = append(chs, NewCategory(g.session, ch)) + case discord.GuildText: + chs = append(chs, NewChannel(g.session, ch)) + } } - container.SetServers(channels) + container.SetServers(chs) return nil } diff --git a/message.go b/message.go index c3460ae..09e7525 100644 --- a/message.go +++ b/message.go @@ -61,7 +61,7 @@ func NewUser(u discord.User) Author { return Author{ id: u.ID, name: text.Rich{Content: u.Username}, - avatar: u.AvatarURL() + "?size=128", + avatar: u.AvatarURL(), } } @@ -85,7 +85,7 @@ func NewGuildMember(m discord.Member, g discord.Guild) Author { return Author{ id: m.User.ID, name: name, - avatar: m.User.AvatarURL() + "?size=128", + avatar: m.User.AvatarURL(), } } diff --git a/segments/md.go b/segments/md.go index a3d571b..87da62b 100644 --- a/segments/md.go +++ b/segments/md.go @@ -45,7 +45,7 @@ func RenderNode(source []byte, n ast.Node) text.Rich { ast.Walk(n, r.renderNode) return text.Rich{ - Content: string(bytes.TrimSpace(buf.Bytes())), + Content: buf.String(), Segments: r.segs, } } @@ -57,14 +57,17 @@ func (r *TextRenderer) i() int { // startBlock guarantees enough indentation to start a new block. func (r *TextRenderer) startBlock() { - var maxNewlines = 2 + var maxNewlines = 0 - // Peek twice. - if r.peekLast(0) == '\n' { - maxNewlines-- - } - if r.peekLast(1) == '\n' { - maxNewlines-- + // Peek twice. If the last character is already a new line or we're only at + // the start of line (length 0), then don't pad. + if r.buf.Len() > 0 { + if r.peekLast(0) != '\n' { + maxNewlines++ + } + if r.peekLast(1) != '\n' { + maxNewlines++ + } } // Write the padding. diff --git a/service.go b/service.go index a220166..1ca0457 100644 --- a/service.go +++ b/service.go @@ -133,21 +133,6 @@ func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) error { return nil } -func (s *Session) Servers(container cchat.ServersContainer) error { - g, err := s.Guilds() - if err != nil { - return err - } - - var servers = make([]cchat.Server, len(g)) - for i := range g { - servers[i] = NewGuild(s, &g[i]) - } - - container.SetServers(servers) - return nil -} - func (s *Session) Disconnect() error { return s.Close() } @@ -157,3 +142,62 @@ func (s *Session) Save() (map[string]string, error) { "token": s.Token, }, nil } + +func (s *Session) Servers(container cchat.ServersContainer) error { + switch { + // If the user has guild folders: + case len(s.Ready.Settings.GuildFolders) > 0: + // TODO: account for missing guilds. + var toplevels = make([]cchat.Server, 0, len(s.Ready.Settings.GuildFolders)) + + for _, folder := range s.Ready.Settings.GuildFolders { + // TODO: correct. + switch { + case folder.ID.Valid(): + fallthrough + case len(folder.GuildIDs) > 1: + toplevels = append(toplevels, NewGuildFolder(s, folder)) + + case len(folder.GuildIDs) == 1: + g, err := NewGuildFromID(s, folder.GuildIDs[0]) + if err != nil { + return errors.Wrap(err, "Failed to get guild in folder") + } + toplevels = append(toplevels, g) + } + } + + container.SetServers(toplevels) + + // If the user doesn't have guild folders but has sorted their guilds + // before: + case len(s.Ready.Settings.GuildPositions) > 0: + var guilds = make([]cchat.Server, 0, len(s.Ready.Settings.GuildPositions)) + + for _, id := range s.Ready.Settings.GuildPositions { + g, err := NewGuildFromID(s, id) + if err != nil { + return errors.Wrap(err, "Failed to get guild in position") + } + guilds = append(guilds, g) + } + + container.SetServers(guilds) + + // None of the above: + default: + g, err := s.Guilds() + if err != nil { + return err + } + + var servers = make([]cchat.Server, len(g)) + for i := range g { + servers[i] = NewGuild(s, &g[i]) + } + + container.SetServers(servers) + } + + return nil +}