diff --git a/channel.go b/channel.go index 160023d..b1e2811 100644 --- a/channel.go +++ b/channel.go @@ -2,6 +2,7 @@ package discord import ( "context" + "time" "github.com/diamondburned/arikawa/api" "github.com/diamondburned/arikawa/discord" @@ -63,13 +64,14 @@ type Channel struct { } var ( - _ cchat.Server = (*Channel)(nil) - _ cchat.ServerMessage = (*Channel)(nil) - _ cchat.ServerMessageSender = (*Channel)(nil) - _ cchat.ServerMessageSendCompleter = (*Channel)(nil) - _ cchat.ServerNickname = (*Channel)(nil) - _ cchat.ServerMessageEditor = (*Channel)(nil) - _ cchat.ServerMessageActioner = (*Channel)(nil) + _ cchat.Server = (*Channel)(nil) + _ cchat.ServerMessage = (*Channel)(nil) + _ cchat.ServerMessageSender = (*Channel)(nil) + _ cchat.ServerMessageSendCompleter = (*Channel)(nil) + _ cchat.ServerNickname = (*Channel)(nil) + _ cchat.ServerMessageEditor = (*Channel)(nil) + _ cchat.ServerMessageActioner = (*Channel)(nil) + _ cchat.ServerMessageTypingIndicator = (*Channel)(nil) ) func NewChannel(s *Session, ch discord.Channel) *Channel { @@ -114,10 +116,10 @@ func (ch *Channel) Name() text.Rich { } } -func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) error { +func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) (func(), error) { // We don't have a nickname if we're not in a guild. if !ch.guildID.Valid() { - return nil + return func() {}, nil } state := ch.session.WithContext(ctx) @@ -125,12 +127,12 @@ func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) e // MemberColor should fill up the state cache. c, err := state.MemberColor(ch.guildID, ch.session.userID) if err != nil { - return errors.Wrap(err, "Failed to get self member color") + return nil, errors.Wrap(err, "Failed to get self member color") } m, err := state.Member(ch.guildID, ch.session.userID) if err != nil { - return errors.Wrap(err, "Failed to get self member") + return nil, errors.Wrap(err, "Failed to get self member") } var rich = text.Rich{Content: m.User.Username} @@ -144,7 +146,29 @@ func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) e } labeler.SetLabel(rich) - return nil + + // Copy the user ID to use. + var selfID = m.User.ID + + return ch.session.AddHandler(func(g *gateway.GuildMemberUpdateEvent) { + if g.GuildID != ch.guildID || g.User.ID != selfID { + return + } + + var rich = text.Rich{Content: m.User.Username} + if m.Nick != "" { + rich.Content = m.Nick + } + + c, err := ch.session.MemberColor(g.GuildID, selfID) + if err == nil { + rich.Segments = []text.Segment{ + segments.NewColored(len(rich.Content), c.Uint32()), + } + } + + labeler.SetLabel(rich) + }), nil } func (ch *Channel) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) { @@ -370,6 +394,10 @@ func (ch *Channel) canManageMessages(userID discord.Snowflake) bool { return p.Has(discord.PermissionManageMessages) } +// CompleteMessage implements message input completion capability for Discord. +// This method supports user mentions, channel mentions and emojis. +// +// For the individual implementations, refer to channel_completion.go. func (ch *Channel) CompleteMessage(words []string, i int) (entries []cchat.CompletionEntry) { var word = words[i] // Word should have at least a character for the char check. @@ -389,6 +417,52 @@ func (ch *Channel) CompleteMessage(words []string, i int) (entries []cchat.Compl return } +func (ch *Channel) Typing() error { + return ch.session.Typing(ch.id) +} + +// TypingTimeout returns 8 seconds. +func (ch *Channel) TypingTimeout() time.Duration { + return 8 * time.Second +} + +func (ch *Channel) TypingSubscribe(ti cchat.TypingIndicator) (func(), error) { + return ch.session.AddHandler(func(t *gateway.TypingStartEvent) { + if t.ChannelID != ch.id { + return + } + + if ch.guildID.Valid() { + g, err := ch.session.Store.Guild(t.GuildID) + if err != nil { + return + } + + if t.Member == nil { + t.Member, err = ch.session.Store.Member(t.GuildID, t.UserID) + if err != nil { + return + } + } + + ti.AddTyper(NewGuildMember(*t.Member, *g)) + return + } + + c, err := ch.self() + if err != nil { + return + } + + for _, user := range c.DMRecipients { + if user.ID == t.UserID { + ti.AddTyper(NewUser(user)) + return + } + } + }), nil +} + func newCancels() func(...func()) []func() { var cancels []func() return func(appended ...func()) []func() { diff --git a/go.mod b/go.mod index d95a831..11d6855 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/diamondburned/arikawa v0.9.5 - github.com/diamondburned/cchat v0.0.35 + github.com/diamondburned/cchat v0.0.37 github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 github.com/go-test/deep v1.0.6 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index cb890a5..e471deb 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,10 @@ github.com/diamondburned/cchat v0.0.34 h1:BGiVxMRA9dmW3rLilIldBvjVan7eTTpaWCCfX9 github.com/diamondburned/cchat v0.0.34/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/cchat v0.0.35 h1:WiMGl8BQJgbP9E4xRxgLGlqUsHpTcJgDKDt8/6a7lBk= github.com/diamondburned/cchat v0.0.35/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= +github.com/diamondburned/cchat v0.0.36 h1:fOD84RV7EUCjoOSogX/Hu5pe4tzHk3Qh7taKaojIAGc= +github.com/diamondburned/cchat v0.0.36/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= +github.com/diamondburned/cchat v0.0.37 h1:yGz9yls5Lb/vLkU/DU53GjC80WOqoRe229DXsu5mtaY= +github.com/diamondburned/cchat v0.0.37/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I= github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/guild.go b/guild.go index 5af4282..e11b041 100644 --- a/guild.go +++ b/guild.go @@ -128,17 +128,25 @@ func (g *Guild) Name() text.Rich { return text.Rich{Content: s.Name} } -func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) error { +func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) { s, err := g.self(ctx) if err != nil { // This shouldn't happen. - return errors.Wrap(err, "Failed to get guild") + return nil, errors.Wrap(err, "Failed to get guild") } - if s.Icon != "" { - iconer.SetIcon(s.IconURL() + "?size=64") + // Used for comparison. + var hash = s.Icon + if hash != "" { + iconer.SetIcon(AvatarURL(s.IconURL())) } - return nil + + return g.session.AddHandler(func(g *gateway.GuildUpdateEvent) { + if g.Icon != hash { + hash = g.Icon + iconer.SetIcon(AvatarURL(s.IconURL())) + } + }), nil } func (g *Guild) Servers(container cchat.ServersContainer) error { diff --git a/message.go b/message.go index e2a4782..e124d0e 100644 --- a/message.go +++ b/message.go @@ -1,6 +1,7 @@ package discord import ( + "net/url" "time" "github.com/diamondburned/arikawa/discord" @@ -51,6 +52,20 @@ func (m messageHeader) Time() time.Time { return m.time.Time() } +// AvatarURL wraps the URL with URL queries for the avatar. +func AvatarURL(URL string) string { + u, err := url.Parse(URL) + if err != nil { + return URL + } + + q := u.Query() + q.Set("size", "64") + u.RawQuery = q.Encode() + + return u.String() +} + type Author struct { id discord.Snowflake name text.Rich @@ -61,7 +76,7 @@ func NewUser(u discord.User) Author { return Author{ id: u.ID, name: text.Rich{Content: u.Username}, - avatar: u.AvatarURL(), + avatar: AvatarURL(u.AvatarURL()), } } @@ -69,7 +84,7 @@ func NewGuildMember(m discord.Member, g discord.Guild) Author { return Author{ id: m.User.ID, name: RenderMemberName(m, g), - avatar: m.User.AvatarURL(), + avatar: AvatarURL(m.User.AvatarURL()), } } diff --git a/service.go b/service.go index 1ca0457..6b2b09f 100644 --- a/service.go +++ b/service.go @@ -4,6 +4,7 @@ import ( "context" "github.com/diamondburned/arikawa/discord" + "github.com/diamondburned/arikawa/gateway" "github.com/diamondburned/arikawa/state" "github.com/diamondburned/cchat" "github.com/diamondburned/cchat/services" @@ -30,9 +31,9 @@ func (Service) Name() text.Rich { return text.Rich{Content: "Discord"} } -func (Service) Icon(ctx context.Context, iconer cchat.IconContainer) error { +func (Service) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) { iconer.SetIcon("https://discord.com/assets/2c21aeda16de354ba5334551a883b481.png") - return nil + return func() {}, nil } func (Service) Authenticate() cchat.Authenticator { @@ -122,15 +123,18 @@ func (s *Session) Name() text.Rich { return text.Rich{Content: u.Username + "#" + u.Discriminator} } -func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) error { +func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) { u, err := s.Me() if err != nil { - return errors.Wrap(err, "Failed to get the current user") + return nil, errors.Wrap(err, "Failed to get the current user") } // Thanks to arikawa, AvatarURL is never empty. - iconer.SetIcon(u.AvatarURL()) - return nil + iconer.SetIcon(AvatarURL(u.AvatarURL())) + + return s.AddHandler(func(u *gateway.UserUpdateEvent) { + iconer.SetIcon(AvatarURL(u.AvatarURL())) + }), nil } func (s *Session) Disconnect() error {