diff --git a/discord.go b/discord.go index b959be8..136fe23 100644 --- a/discord.go +++ b/discord.go @@ -1,9 +1,11 @@ package discord import ( + "context" + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/config" "github.com/diamondburned/cchat-discord/internal/discord/authenticate" - "github.com/diamondburned/cchat-discord/internal/discord/config" "github.com/diamondburned/cchat-discord/internal/discord/session" "github.com/diamondburned/cchat-discord/internal/segments/avatar" "github.com/diamondburned/cchat/services" @@ -29,11 +31,13 @@ type Service struct { empty.Service } -func (Service) Name() text.Rich { - return text.Rich{ +func (Service) Name(_ context.Context, l cchat.LabelContainer) (func(), error) { + l.SetLabel(text.Rich{ Content: "Discord", Segments: []text.Segment{Logo}, - } + }) + + return func() {}, nil } func (Service) Authenticate() []cchat.Authenticator { diff --git a/internal/_discord/channel/channel.go b/internal/_discord/channel/channel.go deleted file mode 100644 index 9537b69..0000000 --- a/internal/_discord/channel/channel.go +++ /dev/null @@ -1,127 +0,0 @@ -package channel - -import ( - "context" - - "github.com/diamondburned/arikawa/v2/discord" - "github.com/diamondburned/arikawa/v2/gateway" - "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" - "github.com/diamondburned/cchat-discord/internal/discord/state" - "github.com/diamondburned/cchat-discord/internal/urlutils" - "github.com/diamondburned/cchat/text" - "github.com/diamondburned/cchat/utils/empty" - "github.com/pkg/errors" -) - -type Channel struct { - empty.Server - shared.Channel - commander cchat.Commander -} - -var _ cchat.Server = (*Channel)(nil) - -func New(s *state.Instance, ch discord.Channel) (cchat.Server, error) { - channel, err := NewChannel(s, ch) - if err != nil { - return nil, err - } - return channel, nil -} - -func NewChannel(s *state.Instance, ch discord.Channel) (Channel, error) { - // Ensure the state keeps the channel's permission. - if ch.GuildID.IsValid() { - _, err := s.Permissions(ch.ID, s.UserID) - if err != nil { - return Channel{}, errors.Wrap(err, "failed to get permission") - } - } - - sharedCh := shared.Channel{ - ID: ch.ID, - GuildID: ch.GuildID, - State: s, - } - - return Channel{ - Channel: sharedCh, - commander: NewCommander(sharedCh), - }, nil -} - -func (ch Channel) ID() cchat.ID { - return ch.Channel.ID.String() -} - -func (ch Channel) Name() text.Rich { - c, err := ch.Self() - if err != nil { - return text.Rich{Content: ch.ID()} - } - - return text.Plain(shared.ChannelName(*c)) -} - -func (ch Channel) AsCommander() cchat.Commander { - return ch.commander -} - -func (ch Channel) AsMessenger() cchat.Messenger { - if !ch.HasPermission(discord.PermissionViewChannel) { - return nil - } - - return message.New(ch.Channel) -} - -func (ch Channel) AsIconer() cchat.Iconer { - // Guild channels never have an icon. - if ch.GuildID.IsValid() { - return nil - } - - c, err := ch.Self() - if err != nil { - return nil - } - - // Only DM channels should have an icon. - if c.Type != discord.DirectMessage { - return nil - } - - return PresenceAvatar{ - user: c.DMRecipients[0], - guild: ch.GuildID, - state: ch.State, - } -} - -type PresenceAvatar struct { - user discord.User - guild discord.GuildID - state *state.Instance -} - -func (avy PresenceAvatar) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) { - if avy.user.Avatar != "" { - iconer.SetIcon(urlutils.AvatarURL(avy.user.AvatarURL())) - } - - // There are so many other places that could be checked, but this is good - // enough. - - c, err := avy.state.Presence(avy.guild, avy.user.ID) - if err == nil && c.User.Avatar != "" { - iconer.SetIcon(urlutils.AvatarURL(c.User.AvatarURL())) - } - - return avy.state.AddHandler(func(update *gateway.PresenceUpdateEvent) { - if avy.user.ID == update.User.ID { - iconer.SetIcon(urlutils.AvatarURL(update.User.AvatarURL())) - } - }), nil -} diff --git a/internal/_discord/channel/message/indicate/typing.go b/internal/_discord/channel/message/indicate/typing.go deleted file mode 100644 index bf41626..0000000 --- a/internal/_discord/channel/message/indicate/typing.go +++ /dev/null @@ -1,44 +0,0 @@ -package indicate - -import ( - "time" - - "github.com/diamondburned/arikawa/v2/gateway" - "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" - "github.com/diamondburned/cchat-discord/internal/discord/channel/typer" - "github.com/diamondburned/cchat-discord/internal/discord/config" -) - -type TypingIndicator struct { - shared.Channel -} - -func NewTyping(ch shared.Channel) cchat.TypingIndicator { - return TypingIndicator{ch} -} - -func (ti TypingIndicator) Typing() error { - if !config.BroadcastTyping() { - return nil - } - - return ti.State.Typing(ti.ID) -} - -// TypingTimeout returns 10 seconds. -func (ti TypingIndicator) TypingTimeout() time.Duration { - return 10 * time.Second -} - -func (ti TypingIndicator) TypingSubscribe(tc cchat.TypingContainer) (func(), error) { - return ti.State.AddHandler(func(t *gateway.TypingStartEvent) { - // Ignore channel mismatch or if the typing event is ours. - if t.ChannelID != ti.ID || t.UserID == ti.State.UserID { - return - } - if typer, err := typer.New(ti.State, t); err == nil { - tc.AddTyper(typer) - } - }), nil -} diff --git a/internal/_discord/channel/message/nickname/nicknamer.go b/internal/_discord/channel/message/nickname/nicknamer.go deleted file mode 100644 index 6a0b61a..0000000 --- a/internal/_discord/channel/message/nickname/nicknamer.go +++ /dev/null @@ -1,92 +0,0 @@ -package nickname - -import ( - "context" - "fmt" - - "github.com/diamondburned/arikawa/v2/discord" - "github.com/diamondburned/arikawa/v2/gateway" - "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" - "github.com/diamondburned/cchat-discord/internal/funcutil" - "github.com/diamondburned/cchat-discord/internal/segments/colored" - "github.com/diamondburned/cchat/text" -) - -type Nicknamer struct { - userID discord.UserID - shared.Channel -} - -func New(ch shared.Channel) cchat.Nicknamer { - return NewMember(ch.State.UserID, ch) -} - -func NewMember(userID discord.UserID, ch shared.Channel) cchat.Nicknamer { - return Nicknamer{userID, ch} -} - -func (nn Nicknamer) Nickname(ctx context.Context, labeler cchat.LabelContainer) (func(), error) { - // We don't have a nickname if we're not in a guild. - if !nn.GuildID.IsValid() { - // Use the current user. - u, err := nn.State.Cabinet.Me() - if err == nil { - labeler.SetLabel(text.Plain(fmt.Sprintf("%s#%s", u.Username, u.Discriminator))) - } - - return func() {}, nil - } - - nn.tryNicknameLabel(ctx, labeler) - - return funcutil.JoinCancels( - nn.State.AddHandler(func(chunks *gateway.GuildMembersChunkEvent) { - if chunks.GuildID != nn.GuildID { - return - } - for _, member := range chunks.Members { - if member.User.ID == nn.userID { - nn.setMember(labeler, member) - break - } - } - }), - nn.State.AddHandler(func(g *gateway.GuildMemberUpdateEvent) { - if g.GuildID == nn.GuildID && g.User.ID == nn.userID { - nn.setMember(labeler, discord.Member{ - User: g.User, - Nick: g.Nick, - RoleIDs: g.RoleIDs, - }) - } - }), - ), nil -} - -func (nn Nicknamer) tryNicknameLabel(ctx context.Context, labeler cchat.LabelContainer) { - state := nn.State.WithContext(ctx) - - m, err := state.Cabinet.Member(nn.GuildID, nn.userID) - if err == nil { - nn.setMember(labeler, *m) - } -} - -func (nn Nicknamer) setMember(labeler cchat.LabelContainer, m discord.Member) { - var rich = text.Rich{Content: m.User.Username} - if m.Nick != "" { - rich.Content = m.Nick - } - - guild, err := nn.State.Cabinet.Guild(nn.GuildID) - if err == nil { - if color := discord.MemberColor(*guild, m); color > 0 { - rich.Segments = []text.Segment{ - colored.New(len(rich.Content), color.Uint32()), - } - } - } - - labeler.SetLabel(rich) -} diff --git a/internal/_discord/folder/folder.go b/internal/_discord/folder/folder.go deleted file mode 100644 index 59cc064..0000000 --- a/internal/_discord/folder/folder.go +++ /dev/null @@ -1,80 +0,0 @@ -package folder - -import ( - "strconv" - "strings" - - "github.com/diamondburned/arikawa/v2/gateway" - "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/guild" - "github.com/diamondburned/cchat-discord/internal/discord/state" - "github.com/diamondburned/cchat-discord/internal/segments/colored" - "github.com/diamondburned/cchat/text" - "github.com/diamondburned/cchat/utils/empty" -) - -type GuildFolder struct { - empty.Server - gateway.GuildFolder - state *state.Instance -} - -func New(s *state.Instance, gf gateway.GuildFolder) cchat.Server { - // Name should never be empty. - if gf.Name == "" { - var names = make([]string, 0, len(gf.GuildIDs)) - - for _, id := range gf.GuildIDs { - g, err := s.Cabinet.Guild(id) - if err == nil { - names = append(names, g.Name) - } - } - - gf.Name = strings.Join(names, ", ") - } - - return &GuildFolder{ - GuildFolder: gf, - state: s, - } -} - -func (gf *GuildFolder) ID() cchat.ID { - return strconv.FormatInt(int64(gf.GuildFolder.ID), 10) -} - -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. - colored.New(len(name.Content), gf.GuildFolder.Color.Uint32()), - } - } - - return name -} - -// IsLister returns true. -func (gf *GuildFolder) AsLister() cchat.Lister { return gf } - -func (gf *GuildFolder) Servers(container cchat.ServersContainer) error { - var servers = make([]cchat.Server, 0, len(gf.GuildIDs)) - - for _, id := range gf.GuildIDs { - g, err := gf.state.Cabinet.Guild(id) - if err != nil { - continue - } - - servers = append(servers, guild.New(gf.state, g)) - } - - container.SetServers(servers) - return nil -} diff --git a/internal/_discord/guild/guild.go b/internal/_discord/guild/guild.go deleted file mode 100644 index ded70d5..0000000 --- a/internal/_discord/guild/guild.go +++ /dev/null @@ -1,124 +0,0 @@ -package guild - -import ( - "context" - "sort" - - "github.com/diamondburned/arikawa/v2/discord" - "github.com/diamondburned/arikawa/v2/gateway" - "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/category" - "github.com/diamondburned/cchat-discord/internal/discord/channel" - "github.com/diamondburned/cchat-discord/internal/discord/state" - "github.com/diamondburned/cchat-discord/internal/urlutils" - "github.com/diamondburned/cchat/text" - "github.com/diamondburned/cchat/utils/empty" - "github.com/pkg/errors" -) - -type Guild struct { - empty.Server - id discord.GuildID - state *state.Instance -} - -func New(s *state.Instance, g *discord.Guild) cchat.Server { - return &Guild{ - id: g.ID, - state: s, - } -} - -func NewFromID(s *state.Instance, gID discord.GuildID) (cchat.Server, error) { - g, err := s.Cabinet.Guild(gID) - if err != nil { - return nil, err - } - - return New(s, g), nil -} - -func (g *Guild) self() (*discord.Guild, error) { - return g.state.Cabinet.Guild(g.id) -} - -func (g *Guild) ID() cchat.ID { - return g.id.String() -} - -func (g *Guild) Name() text.Rich { - s, err := g.self() - if err != nil { - // This shouldn't happen. - return text.Rich{Content: g.id.String()} - } - - return text.Rich{Content: s.Name} -} - -func (g *Guild) AsIconer() cchat.Iconer { return g } - -func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) { - s, err := g.self() - if err != nil { - // This shouldn't happen. - return nil, errors.Wrap(err, "Failed to get guild") - } - - // Used for comparison. - if s.Icon != "" { - iconer.SetIcon(urlutils.AvatarURL(s.IconURL())) - } - - return g.state.AddHandler(func(update *gateway.GuildUpdateEvent) { - if g.id == update.ID { - iconer.SetIcon(urlutils.AvatarURL(s.IconURL())) - } - }), nil -} - -func (g *Guild) AsLister() cchat.Lister { return g } - -func (g *Guild) Servers(container cchat.ServersContainer) error { - c, err := g.state.Channels(g.id) - if err != nil { - return errors.Wrap(err, "Failed to get channels") - } - - // Only get top-level channels (those with category ID being null). - var toplevels = category.FilterAccessible(g.state, category.FilterCategory(c, 0)) - - // Sort so that positions are correct. - sort.SliceStable(toplevels, func(i, j int) bool { - return toplevels[i].Position < toplevels[j].Position - }) - - // Sort so that channels are before categories. - sort.SliceStable(toplevels, func(i, _ int) bool { - return toplevels[i].Type != discord.GuildCategory - }) - - chs := make([]cchat.Server, 0, len(toplevels)) - ids := make(map[discord.ChannelID]struct{}, len(toplevels)) - - for _, ch := range toplevels { - switch ch.Type { - case discord.GuildCategory: - chs = append(chs, category.New(g.state, ch)) - case discord.GuildText: - c, err := channel.New(g.state, ch) - if err != nil { - return errors.Wrapf(err, "Failed to make channel %q: %v", ch.Name, err) - } - chs = append(chs, c) - default: - continue - } - } - - container.SetServers(chs) - - // TODO: account for insertion/deletion. - - return nil -} diff --git a/internal/_discord/session/session.go b/internal/_discord/session/session.go deleted file mode 100644 index 00100ab..0000000 --- a/internal/_discord/session/session.go +++ /dev/null @@ -1,156 +0,0 @@ -package session - -import ( - "context" - - "github.com/diamondburned/arikawa/v2/gateway" - "github.com/diamondburned/arikawa/v2/session" - "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/folder" - "github.com/diamondburned/cchat-discord/internal/discord/guild" - "github.com/diamondburned/cchat-discord/internal/discord/private" - "github.com/diamondburned/cchat-discord/internal/discord/state" - "github.com/diamondburned/cchat-discord/internal/urlutils" - "github.com/diamondburned/cchat/text" - "github.com/diamondburned/cchat/utils/empty" - "github.com/diamondburned/ningen/v2" - "github.com/pkg/errors" -) - -var ErrMFA = session.ErrMFA - -type Session struct { - empty.Session - private cchat.Server - state *state.Instance -} - -func NewFromInstance(i *state.Instance) (cchat.Session, error) { - priv, err := private.New(i) - if err != nil { - return nil, errors.Wrap(err, "failed to make main private server") - } - - return &Session{ - private: priv, - state: i, - }, nil -} - -func (s *Session) ID() cchat.ID { - return s.state.UserID.String() -} - -func (s *Session) Name() text.Rich { - u, err := s.state.Cabinet.Me() - if err != nil { - // This shouldn't happen, ever. - return text.Rich{Content: "<@" + s.state.UserID.String() + ">"} - } - - return text.Rich{Content: u.Username + "#" + u.Discriminator} -} - -func (s *Session) AsIconer() cchat.Iconer { return s } - -func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) { - u, err := s.state.Me() - if err != nil { - return nil, errors.Wrap(err, "Failed to get the current user") - } - - // Thanks to arikawa, AvatarURL is never empty. - iconer.SetIcon(urlutils.AvatarURL(u.AvatarURL())) - - return s.state.AddHandler(func(*gateway.UserUpdateEvent) { - // Bypass the event and use the state cache. - if u, err := s.state.Cabinet.Me(); err == nil { - iconer.SetIcon(urlutils.AvatarURL(u.AvatarURL())) - } - }), nil -} - -func (s *Session) Disconnect() error { - return s.state.Close() -} - -func (s *Session) AsSessionSaver() cchat.SessionSaver { return s.state } - -func (s *Session) Servers(container cchat.ServersContainer) error { - // Reset the entire container when the session is closed. - s.state.AddHandler(func(*session.Closed) { - container.SetServers(nil) - }) - - // Set the entire container again once reconnected. - s.state.AddHandler(func(*ningen.Connected) { - s.servers(container) - }) - - return s.servers(container) -} - -func (s *Session) servers(container cchat.ServersContainer) error { - ready := s.state.Ready() - - // If the user has guild folders: - if len(ready.UserSettings.GuildFolders) > 0 { - // TODO: account for missing guilds. - toplevels := make([]cchat.Server, 1, len(ready.UserSettings.GuildFolders)+1) - toplevels[0] = s.private - - for _, guildFolder := range ready.UserSettings.GuildFolders { - // TODO: correct. - switch { - case guildFolder.ID != 0: - fallthrough - case len(guildFolder.GuildIDs) > 1: - toplevels = append(toplevels, folder.New(s.state, guildFolder)) - - case len(guildFolder.GuildIDs) == 1: - g, err := guild.NewFromID(s.state, guildFolder.GuildIDs[0]) - if err != nil { - continue - } - toplevels = append(toplevels, g) - } - } - - container.SetServers(toplevels) - return nil - } - - // If the user doesn't have guild folders but has sorted their guilds - // before: - if len(ready.UserSettings.GuildPositions) > 0 { - guilds := make([]cchat.Server, 1, len(ready.UserSettings.GuildPositions)+1) - guilds[0] = s.private - - for _, id := range ready.UserSettings.GuildPositions { - g, err := guild.NewFromID(s.state, id) - if err != nil { - continue - } - guilds = append(guilds, g) - } - - container.SetServers(guilds) - return nil - } - - // None of the above: - g, err := s.state.Guilds() - if err != nil { - return err - } - - servers := make([]cchat.Server, len(g)+1) - servers[0] = s.private - - for i := range g { - servers[i+1] = guild.New(s.state, &g[i]) - } - - container.SetServers(servers) - return nil -} diff --git a/internal/_discord/state/state.go b/internal/_discord/state/state.go deleted file mode 100644 index c3a9bc7..0000000 --- a/internal/_discord/state/state.go +++ /dev/null @@ -1,116 +0,0 @@ -// Package state provides a shared state instance for other packages to use. -package state - -import ( - "context" - - "github.com/diamondburned/arikawa/v2/discord" - "github.com/diamondburned/arikawa/v2/session" - "github.com/diamondburned/arikawa/v2/state" - "github.com/diamondburned/arikawa/v2/state/store/defaultstore" - "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/state/nonce" - "github.com/diamondburned/ningen/v2" - "github.com/pkg/errors" -) - -type Instance struct { - *ningen.State - Nonces *nonce.Map - - // UserID is a constant user ID of the current user. It is guaranteed to be - // valid. - UserID discord.UserID -} - -var _ cchat.SessionSaver = (*Instance)(nil) - -// ErrInvalidSession is returned if SessionRestore is given a bad session. -var ErrInvalidSession = errors.New("invalid session") - -func NewFromData(data map[string]string) (*Instance, error) { - tk, ok := data["token"] - if !ok { - return nil, ErrInvalidSession - } - - return NewFromToken(tk) -} - -func NewFromToken(token string) (*Instance, error) { - s, err := state.New(token) - if err != nil { - return nil, err - } - - return New(s) -} - -func Login(email, password, mfa string) (*Instance, error) { - session, err := session.Login(email, password, mfa) - if err != nil { - return nil, err - } - - cabinet := defaultstore.New() - cabinet.MessageStore = defaultstore.NewMessage(50) - - return New(state.NewFromSession(session, cabinet)) -} - -func New(s *state.State) (*Instance, error) { - n, err := ningen.FromState(s) - if err != nil { - return nil, errors.Wrap(err, "failed to create a state wrapper") - } - - // n.Client.OnRequest = append(n.Client.OnRequest, - // func(r httpdriver.Request) error { - // log.Println("[Discord] Request", r.GetPath()) - // return nil - // }, - // ) - - if err := n.Open(); err != nil { - return nil, err - } - - // Prefetch user. - u, err := s.Me() - if err != nil { - return nil, errors.Wrap(err, "failed to get current user") - } - - return &Instance{ - UserID: u.ID, - State: n, - Nonces: new(nonce.Map), - }, nil -} - -// Permissions queries for the permission without hitting the REST API. -func (s *Instance) Permissions( - chID discord.ChannelID, uID discord.UserID) (discord.Permissions, error) { - - return s.StateOnly().Permissions(chID, uID) -} - -var deadCtx = expiredContext() - -// StateOnly returns a shallow copy of *State with an already-expired context. -func (s *Instance) StateOnly() *state.State { - return s.WithContext(deadCtx) -} - -func (s *Instance) SaveSession() map[string]string { - return map[string]string{ - "token": s.Token, - } -} - -func expiredContext() context.Context { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - return ctx -} diff --git a/internal/config/boolstamp.go b/internal/config/boolstamp.go new file mode 100644 index 0000000..1c5a6f9 --- /dev/null +++ b/internal/config/boolstamp.go @@ -0,0 +1,39 @@ +package config + +import ( + "strconv" + "time" + + "github.com/pkg/errors" +) + +type boolStamp struct { + stamp time.Duration + value bool +} + +var _ customType = (*boolStamp)(nil) + +func (bs boolStamp) Marshal() string { + if bs.stamp > 0 { + return bs.stamp.String() + } + + return strconv.FormatBool(bs.value) +} + +func (bs *boolStamp) Unmarshal(v string) error { + t, err := time.ParseDuration(v) + if err == nil && t > 0 { + bs.stamp = t + return nil + } + + b, err := strconv.ParseBool(v) + if err == nil { + bs.value = b + return nil + } + + return errors.New("invalid bool or timestamp") +} diff --git a/internal/_discord/config/config.go b/internal/config/config.go similarity index 77% rename from internal/_discord/config/config.go rename to internal/config/config.go index a5ffa93..bc00063 100644 --- a/internal/_discord/config/config.go +++ b/internal/config/config.go @@ -9,21 +9,9 @@ import ( "github.com/pkg/errors" ) -var World = ®istry{ - configs: []config{ - {"Mention on Reply", true}, - {"Broadcast Typing", true}, - }, -} - -// MentionOnReply returns true if message replies should mention users. -func MentionOnReply() bool { - return World.get(0).(bool) -} - -// BroadcastTyping returns true if typing events should be broadcasted. -func BroadcastTyping() bool { - return World.get(1).(bool) +type customType interface { + Marshal() string + Unmarshal(string) error } type config struct { @@ -55,13 +43,14 @@ func (c *config) Unmarshal(src map[string]string) (err error) { } } - var v interface{} - - switch c.Value.(type) { + switch v := c.Value.(type) { case bool: - v, err = strconv.ParseBool(strVal) + c.Value, err = strconv.ParseBool(strVal) case string: - v = strVal + c.Value = strVal + case customType: + err = v.Unmarshal(strVal) + c.Value = v default: err = fmt.Errorf("unknown type %T", c.Value) } @@ -73,7 +62,6 @@ func (c *config) Unmarshal(src map[string]string) (err error) { } } - c.Value = v return nil } diff --git a/internal/config/world.go b/internal/config/world.go new file mode 100644 index 0000000..4d24193 --- /dev/null +++ b/internal/config/world.go @@ -0,0 +1,32 @@ +package config + +import ( + "time" + + "github.com/diamondburned/cchat" +) + +var World cchat.Configurator = world + +var world = ®istry{ + configs: []config{ + {"Mention on Reply", &boolStamp{stamp: 5 * time.Minute, value: false}}, + {"Broadcast Typing", true}, + }, +} + +// MentionOnReply returns true if message replies should mention users. +func MentionOnReply(timestamp time.Time) bool { + v := world.get(0).(boolStamp) + + if v.stamp > 0 { + return timestamp.Add(v.stamp).Before(time.Now()) + } + + return v.value +} + +// BroadcastTyping returns true if typing events should be broadcasted. +func BroadcastTyping() bool { + return world.get(1).(bool) +} diff --git a/internal/_discord/authenticate/authenticator.go b/internal/discord/authenticate/authenticator.go similarity index 100% rename from internal/_discord/authenticate/authenticator.go rename to internal/discord/authenticate/authenticator.go diff --git a/internal/_discord/authenticate/discordlogin.go b/internal/discord/authenticate/discordlogin.go similarity index 100% rename from internal/_discord/authenticate/discordlogin.go rename to internal/discord/authenticate/discordlogin.go diff --git a/internal/_discord/authenticate/login.go b/internal/discord/authenticate/login.go similarity index 100% rename from internal/_discord/authenticate/login.go rename to internal/discord/authenticate/login.go diff --git a/internal/_discord/authenticate/token.go b/internal/discord/authenticate/token.go similarity index 100% rename from internal/_discord/authenticate/token.go rename to internal/discord/authenticate/token.go diff --git a/internal/_discord/authenticate/totp.go b/internal/discord/authenticate/totp.go similarity index 100% rename from internal/_discord/authenticate/totp.go rename to internal/discord/authenticate/totp.go diff --git a/internal/_discord/message/author.go b/internal/discord/message/author.go similarity index 70% rename from internal/_discord/message/author.go rename to internal/discord/message/author.go index 1ce0ef2..cd851e8 100644 --- a/internal/_discord/message/author.go +++ b/internal/discord/message/author.go @@ -1,9 +1,10 @@ package message import ( + "context" + "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/segments/colored" "github.com/diamondburned/cchat-discord/internal/segments/mention" @@ -13,22 +14,26 @@ import ( ) type Author struct { - name text.Rich - user *mention.User // same pointer as in name + name text.Rich + user *mention.User // same pointer as in name + state *state.Instance } var _ cchat.User = (*Author)(nil) // NewAuthor creates a new message author. -func NewAuthor(user *mention.User) Author { +func NewAuthor(s *state.Instance, user *mention.User) Author { + user.WithState(s.State) + return Author{ - name: RenderAuthorName(user), - user: user, + name: RenderUserName(user), + user: user, + state: s, } } -// RenderAuthorName renders the given user mention into a text segment. -func RenderAuthorName(user *mention.User) text.Rich { +// RenderUserName renders the given user mention into a text segment. +func RenderUserName(user *mention.User) text.Rich { var rich text.Rich richUser(&rich, user) return rich @@ -46,7 +51,11 @@ func richUser(rich *text.Rich, user *mention.User) (start, end int) { ) } - rich.Segments = append(rich.Segments, mention.NewSegment(start, end, user)) + rich.Segments = append(rich.Segments, mention.Segment{ + Start: start, + End: end, + User: user, + }) return } @@ -55,17 +64,18 @@ func (a Author) ID() cchat.ID { return a.user.UserID().String() } -func (a Author) Name() text.Rich { - return a.name -} +// Name subscribes the author to the global name label registry. +func (a Author) Name(_ context.Context, l cchat.LabelContainer) (func(), error) { + if guildID := a.user.GuildID(); guildID.IsValid() { + return a.state.Labels.AddMemberLabel(guildID, a.user.UserID(), l), nil + } -func (a Author) Avatar() string { - return a.user.Avatar() + return a.state.Labels.AddPresenceLabel(a.user.UserID(), l), nil } const authorReplyingTo = " replying to " -// AddUserReply modifies Author to make it appear like it's a message reply. +// AddUserReply modifies User to make it appear like it's a message reply. // Specifically, this function is used for direct messages in virtual channels. func (a *Author) AddUserReply(user discord.User, s *state.Instance) { a.name.Content += authorReplyingTo @@ -87,7 +97,7 @@ func (a *Author) AddChannelReply(ch discord.Channel, s *state.Instance) { } a.name.Content += authorReplyingTo - start, end := segutil.Write(&a.name, shared.ChannelName(ch)) + start, end := segutil.Write(&a.name, mention.ChannelName(ch)) a.name.Segments = append(a.name.Segments, mention.Segment{ @@ -98,7 +108,7 @@ func (a *Author) AddChannelReply(ch discord.Channel, s *state.Instance) { ) } -// AddMessageReference adds a message reference to the author. +// AddMessageReference adds a message reference to the user. func (a *Author) AddMessageReference(ref discord.Message, s *state.Instance) { a.name.Content += authorReplyingTo diff --git a/internal/_discord/message/message.go b/internal/discord/message/message.go similarity index 97% rename from internal/_discord/message/message.go rename to internal/discord/message/message.go index 7a307f3..d068741 100644 --- a/internal/_discord/message/message.go +++ b/internal/discord/message/message.go @@ -100,7 +100,7 @@ func NewGuildMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Mes user.Prefetch() - return NewMessage(message, s, NewAuthor(user)) + return NewMessage(message, s, NewAuthor(s, user)) } // NewBacklogMessage uses the session to create a message fetched from the @@ -118,7 +118,7 @@ func NewBacklogMessage(m discord.Message, s *state.Instance) Message { user.WithState(s.State) user.Prefetch() - return NewMessage(m, s, NewAuthor(user)) + return NewMessage(m, s, NewAuthor(s, user)) } // NewDirectMessage creates a new direct message. @@ -127,7 +127,7 @@ func NewDirectMessage(m discord.Message, s *state.Instance) Message { user.WithState(s.State) user.Prefetch() - return NewMessage(m, s, NewAuthor(user)) + return NewMessage(m, s, NewAuthor(s, user)) } // NewAuthorUpdate creates a new message that contains a new author. @@ -137,7 +137,7 @@ func NewAuthorUpdate(msg discord.Message, m discord.Member, s *state.Instance) M user.WithGuildID(msg.GuildID) user.WithMember(m) - author := NewAuthor(user) + author := NewAuthor(s, user) if ref := ReferencedMessage(msg, s, true); ref != nil { author.AddMessageReference(*ref, s) } @@ -303,7 +303,7 @@ func newRegularContent(m discord.Message, s *state.Instance) Message { } } -func (m Message) Author() cchat.Author { +func (m Message) Author() cchat.User { if m.author.user == nil { return nil } @@ -374,7 +374,11 @@ func segmentFuncFromMention(m discord.Message, s *state.Instance) func(i, j int) user.Prefetch() - return mention.NewSegment(i, j, user) + return mention.Segment{ + Start: i, + End: j, + User: user, + } } } diff --git a/internal/_discord/category/category.go b/internal/discord/session/channel/category/category.go similarity index 80% rename from internal/_discord/category/category.go rename to internal/discord/session/channel/category/category.go index 0f7fa21..5acc2a5 100644 --- a/internal/_discord/category/category.go +++ b/internal/discord/session/channel/category/category.go @@ -1,13 +1,13 @@ package category import ( + "context" "sort" "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel" "github.com/diamondburned/cchat-discord/internal/discord/state" - "github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/utils/empty" "github.com/pkg/errors" ) @@ -76,24 +76,16 @@ func (c *Category) ID() cchat.ID { return c.id.String() } -func (c *Category) Name() text.Rich { - t, err := c.state.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) Name(_ context.Context, l cchat.LabelContainer) (func(), error) { + return c.state.Labels.AddChannelLabel(c.id, l), nil } func (c *Category) AsLister() cchat.Lister { return c } -func (c *Category) Servers(container cchat.ServersContainer) error { +func (c *Category) Servers(container cchat.ServersContainer) (func(), error) { t, err := c.state.Channels(c.guildID) if err != nil { - return errors.Wrap(err, "Failed to get channels") + return nil, errors.Wrap(err, "Failed to get channels") } // Filter out channels with this category ID. @@ -107,12 +99,12 @@ func (c *Category) Servers(container cchat.ServersContainer) error { for i := range chs { c, err := channel.New(c.state, chs[i]) if err != nil { - return errors.Wrapf(err, "Failed to make channel %s: %v", chs[i].Name, err) + return nil, errors.Wrapf(err, "Failed to make channel %s: %v", chs[i].Name, err) } chv[i] = c } container.SetServers(chv) - return nil + return func() {}, nil } diff --git a/internal/discord/session/channel/channel.go b/internal/discord/session/channel/channel.go new file mode 100644 index 0000000..c6e882d --- /dev/null +++ b/internal/discord/session/channel/channel.go @@ -0,0 +1,70 @@ +package channel + +import ( + "context" + + "github.com/diamondburned/arikawa/v2/discord" + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat/utils/empty" + "github.com/pkg/errors" +) + +type Channel struct { + empty.Server + shared.Channel + commander cchat.Commander +} + +var _ cchat.Server = (*Channel)(nil) + +func New(s *state.Instance, ch discord.Channel) (cchat.Server, error) { + channel, err := NewChannel(s, ch) + if err != nil { + return nil, err + } + return channel, nil +} + +func NewChannel(s *state.Instance, ch discord.Channel) (Channel, error) { + // Ensure the state keeps the channel's permission. + if ch.GuildID.IsValid() { + _, err := s.Permissions(ch.ID, s.UserID) + if err != nil { + return Channel{}, errors.Wrap(err, "failed to get permission") + } + } + + sharedCh := shared.Channel{ + ID: ch.ID, + GuildID: ch.GuildID, + State: s, + } + + return Channel{ + Channel: sharedCh, + commander: NewCommander(sharedCh), + }, nil +} + +func (ch Channel) ID() cchat.ID { + return ch.Channel.ID.String() +} + +func (ch Channel) Name(_ context.Context, l cchat.LabelContainer) (func(), error) { + return ch.State.Labels.AddChannelLabel(ch.Channel.ID, l), nil +} + +func (ch Channel) AsCommander() cchat.Commander { + return ch.commander +} + +func (ch Channel) AsMessenger() cchat.Messenger { + if !ch.HasPermission(discord.PermissionViewChannel) { + return nil + } + + return messenger.New(ch.Channel) +} diff --git a/internal/_discord/channel/commander.go b/internal/discord/session/channel/commander.go similarity index 81% rename from internal/_discord/channel/commander.go rename to internal/discord/session/channel/commander.go index e594250..e6dea11 100644 --- a/internal/_discord/channel/commander.go +++ b/internal/discord/session/channel/commander.go @@ -4,21 +4,21 @@ import ( "strings" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/commands" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/commands" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/sender/completer" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/diamondburned/cchat/text" ) type Commander struct { shared.Channel - msgCompl complete.ChannelCompleter + msgCompl completer.ChannelCompleter } func NewCommander(ch shared.Channel) cchat.Commander { return Commander{ Channel: ch, - msgCompl: complete.ChannelCompleter{ + msgCompl: completer.ChannelCompleter{ Channel: ch, }, } diff --git a/internal/_discord/channel/commands/arguments.go b/internal/discord/session/channel/commands/arguments.go similarity index 100% rename from internal/_discord/channel/commands/arguments.go rename to internal/discord/session/channel/commands/arguments.go diff --git a/internal/_discord/channel/commands/command.go b/internal/discord/session/channel/commands/command.go similarity index 82% rename from internal/_discord/channel/commands/command.go rename to internal/discord/session/channel/commands/command.go index 295b844..7fa5a64 100644 --- a/internal/_discord/channel/commands/command.go +++ b/internal/discord/session/channel/commands/command.go @@ -3,7 +3,7 @@ package commands import ( "bytes" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" ) type Command struct { diff --git a/internal/_discord/channel/commands/commands.go b/internal/discord/session/channel/commands/commands.go similarity index 98% rename from internal/_discord/channel/commands/commands.go rename to internal/discord/session/channel/commands/commands.go index c82ccff..c7ca3ee 100644 --- a/internal/_discord/channel/commands/commands.go +++ b/internal/discord/session/channel/commands/commands.go @@ -10,7 +10,7 @@ import ( "github.com/diamondburned/arikawa/v2/bot/extras/arguments" "github.com/diamondburned/arikawa/v2/discord" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/pkg/errors" ) diff --git a/internal/_discord/channel/message/action/actioner.go b/internal/discord/session/channel/messenger/actioner/actioner.go similarity index 95% rename from internal/_discord/channel/message/action/actioner.go rename to internal/discord/session/channel/messenger/actioner/actioner.go index c93d183..b8cbcbb 100644 --- a/internal/_discord/channel/message/action/actioner.go +++ b/internal/discord/session/channel/messenger/actioner/actioner.go @@ -1,9 +1,9 @@ -package action +package actioner import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/pkg/errors" ) diff --git a/internal/_discord/channel/message/backlog/backlogger.go b/internal/discord/session/channel/messenger/backlogger/backlogger.go similarity index 89% rename from internal/_discord/channel/message/backlog/backlogger.go rename to internal/discord/session/channel/messenger/backlogger/backlogger.go index d62235b..18ecf1f 100644 --- a/internal/_discord/channel/message/backlog/backlogger.go +++ b/internal/discord/session/channel/messenger/backlogger/backlogger.go @@ -1,12 +1,12 @@ -package backlog +package backlogger import ( "context" "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/message" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/pkg/errors" ) diff --git a/internal/_discord/channel/message/edit/editor.go b/internal/discord/session/channel/messenger/editor/editor.go similarity index 92% rename from internal/_discord/channel/message/edit/editor.go rename to internal/discord/session/channel/messenger/editor/editor.go index 3684eb8..eb0097f 100644 --- a/internal/_discord/channel/message/edit/editor.go +++ b/internal/discord/session/channel/messenger/editor/editor.go @@ -1,9 +1,9 @@ -package edit +package editor import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/pkg/errors" ) diff --git a/internal/_discord/channel/typer/typer.go b/internal/discord/session/channel/messenger/indicator/typing.go similarity index 52% rename from internal/_discord/channel/typer/typer.go rename to internal/discord/session/channel/messenger/indicator/typing.go index 2551d03..ab0f7da 100644 --- a/internal/_discord/channel/typer/typer.go +++ b/internal/discord/session/channel/messenger/indicator/typing.go @@ -1,26 +1,53 @@ -package typer +package indicator import ( "time" - "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/config" "github.com/diamondburned/cchat-discord/internal/discord/message" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/pkg/errors" ) -type Typer struct { - message.Author - time discord.UnixTimestamp +type TypingIndicator struct { + shared.Channel } -var _ cchat.Typer = (*Typer)(nil) +func NewTyping(ch shared.Channel) cchat.TypingIndicator { + return TypingIndicator{ch} +} + +func (ti TypingIndicator) Typing() error { + if !config.BroadcastTyping() { + return nil + } + + return ti.State.Typing(ti.ID) +} + +// TypingTimeout returns 10 seconds. +func (ti TypingIndicator) TypingTimeout() time.Duration { + return 10 * time.Second +} + +func (ti TypingIndicator) TypingSubscribe(tc cchat.TypingContainer) (func(), error) { + return ti.State.AddHandler(func(t *gateway.TypingStartEvent) { + // Ignore channel mismatch or if the typing event is ours. + if t.ChannelID != ti.ID || t.UserID == ti.State.UserID { + return + } + if typer, err := NewTyperUser(ti.State, t); err == nil { + tc.AddTyper(typer) + } + }), nil +} // New creates a new Typer that satisfies cchat.Typer. -func New(s *state.Instance, ev *gateway.TypingStartEvent) (*Typer, error) { +func NewTyperUser(s *state.Instance, ev *gateway.TypingStartEvent) (cchat.User, error) { var user *mention.User if ev.GuildID.IsValid() { @@ -56,12 +83,5 @@ func New(s *state.Instance, ev *gateway.TypingStartEvent) (*Typer, error) { user.WithState(s.State) user.Prefetch() - return &Typer{ - Author: message.NewAuthor(user), - time: ev.Timestamp, - }, nil -} - -func (t Typer) Time() time.Time { - return t.time.Time() + return message.NewAuthor(s, user), nil } diff --git a/internal/_discord/channel/message/indicate/unread.go b/internal/discord/session/channel/messenger/indicator/unread.go similarity index 93% rename from internal/_discord/channel/message/indicate/unread.go rename to internal/discord/session/channel/messenger/indicator/unread.go index 13c61f7..9d75539 100644 --- a/internal/_discord/channel/message/indicate/unread.go +++ b/internal/discord/session/channel/messenger/indicator/unread.go @@ -1,9 +1,9 @@ -package indicate +package indicator import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/diamondburned/ningen/v2/states/read" "github.com/pkg/errors" ) diff --git a/internal/_discord/channel/message/memberlist/member.go b/internal/discord/session/channel/messenger/memberlister/member.go similarity index 91% rename from internal/_discord/channel/message/memberlist/member.go rename to internal/discord/session/channel/messenger/memberlister/member.go index 98d9010..1dc32ba 100644 --- a/internal/_discord/channel/message/memberlist/member.go +++ b/internal/discord/session/channel/messenger/memberlister/member.go @@ -1,4 +1,4 @@ -package memberlist +package memberlister import ( "context" @@ -8,7 +8,7 @@ import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/diamondburned/cchat-discord/internal/segments/emoji" "github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat/text" @@ -40,18 +40,21 @@ func (l *Member) ID() cchat.ID { return l.mention.UserID().String() } -func (l *Member) Name(ctx context.Context, labeler cchat.LabelContainer) error { +func (l *Member) Name(ctx context.Context, labeler cchat.LabelContainer) (func(), error) { l.mention.Prefetch() content := l.mention.DisplayName() labeler.SetLabel(text.Rich{ Content: content, Segments: []text.Segment{ - mention.NewSegment(0, len(content), &l.mention), + mention.Segment{ + End: len(content), + User: &l.mention, + }, }, }) - return nil + return func() {}, nil } func (l *Member) Status() cchat.Status { diff --git a/internal/_discord/channel/message/memberlist/memberlist.go b/internal/discord/session/channel/messenger/memberlister/memberlist.go similarity index 96% rename from internal/_discord/channel/message/memberlist/memberlist.go rename to internal/discord/session/channel/messenger/memberlister/memberlist.go index 65f32e7..9b58d14 100644 --- a/internal/_discord/channel/message/memberlist/memberlist.go +++ b/internal/discord/session/channel/messenger/memberlister/memberlist.go @@ -1,11 +1,11 @@ -package memberlist +package memberlister import ( "context" "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/diamondburned/ningen/v2/states/member" ) diff --git a/internal/_discord/channel/message/memberlist/section.go b/internal/discord/session/channel/messenger/memberlister/section.go similarity index 92% rename from internal/_discord/channel/message/memberlist/section.go rename to internal/discord/session/channel/messenger/memberlister/section.go index b383b4b..44d305c 100644 --- a/internal/_discord/channel/message/memberlist/section.go +++ b/internal/discord/session/channel/messenger/memberlister/section.go @@ -1,4 +1,4 @@ -package memberlist +package memberlister import ( "context" @@ -7,7 +7,7 @@ import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/diamondburned/cchat/text" ) @@ -61,9 +61,9 @@ func (s Section) ID() cchat.ID { return s.id } -func (s Section) Name(ctx context.Context, labeler cchat.LabelContainer) (err error) { +func (s Section) Name(ctx context.Context, labeler cchat.LabelContainer) (func(), error) { labeler.SetLabel(text.Plain(s.name)) - return nil + return func() {}, nil } func (s Section) Total() int { diff --git a/internal/_discord/channel/message/message.go b/internal/discord/session/channel/messenger/messenger.go similarity index 78% rename from internal/_discord/channel/message/message.go rename to internal/discord/session/channel/messenger/messenger.go index 3e6a69e..7786f44 100644 --- a/internal/_discord/channel/message/message.go +++ b/internal/discord/session/channel/messenger/messenger.go @@ -1,4 +1,4 @@ -package message +package messenger import ( "context" @@ -7,15 +7,14 @@ import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/action" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/backlog" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/edit" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/indicate" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/memberlist" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/nickname" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/send" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/message" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/actioner" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/backlogger" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/editor" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/indicator" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/memberlister" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/sender" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/diamondburned/cchat-discord/internal/funcutil" "github.com/diamondburned/cchat/utils/empty" ) @@ -107,7 +106,7 @@ func (msgr *Messenger) AsSender() cchat.Sender { return nil } - return send.New(msgr.Channel) + return sender.New(msgr.Channel) } func (msgr *Messenger) AsEditor() cchat.Editor { @@ -115,22 +114,22 @@ func (msgr *Messenger) AsEditor() cchat.Editor { return nil } - return edit.New(msgr.Channel) + return editor.New(msgr.Channel) } func (msgr *Messenger) AsActioner() cchat.Actioner { - return action.New(msgr.Channel) + return actioner.New(msgr.Channel) } func (msgr *Messenger) AsNicknamer() cchat.Nicknamer { - return nickname.New(msgr.Channel) + return NewMeNicknamer(msgr.Channel) } func (msgr *Messenger) AsMemberLister() cchat.MemberLister { if !msgr.GuildID.IsValid() { return nil } - return memberlist.New(msgr.Channel) + return memberlister.New(msgr.Channel) } func (msgr *Messenger) AsBacklogger() cchat.Backlogger { @@ -138,13 +137,13 @@ func (msgr *Messenger) AsBacklogger() cchat.Backlogger { return nil } - return backlog.New(msgr.Channel) + return backlogger.New(msgr.Channel) } func (msgr *Messenger) AsTypingIndicator() cchat.TypingIndicator { - return indicate.NewTyping(msgr.Channel) + return indicator.NewTyping(msgr.Channel) } func (msgr *Messenger) AsUnreadIndicator() cchat.UnreadIndicator { - return indicate.NewUnread(msgr.Channel) + return indicator.NewUnread(msgr.Channel) } diff --git a/internal/discord/session/channel/messenger/nicknamer.go b/internal/discord/session/channel/messenger/nicknamer.go new file mode 100644 index 0000000..ec54243 --- /dev/null +++ b/internal/discord/session/channel/messenger/nicknamer.go @@ -0,0 +1,28 @@ +package messenger + +import ( + "context" + + "github.com/diamondburned/arikawa/v2/discord" + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" +) + +type nicknamer struct { + userID discord.UserID + shared.Channel +} + +// New creates a new nicknamer for self. +func NewMeNicknamer(ch shared.Channel) cchat.Nicknamer { + return NewUserNicknamer(ch.State.UserID, ch) +} + +// NewUserNicknamer creates a new nicknamer for the given user ID. +func NewUserNicknamer(userID discord.UserID, ch shared.Channel) cchat.Nicknamer { + return nicknamer{userID, ch} +} + +func (nn nicknamer) Nickname(ctx context.Context, labeler cchat.LabelContainer) (func(), error) { + return nn.State.Labels.AddMemberLabel(nn.GuildID, nn.userID, labeler), nil +} diff --git a/internal/_discord/channel/message/send/complete/channel.go b/internal/discord/session/channel/messenger/sender/completer/channel.go similarity index 92% rename from internal/_discord/channel/message/send/complete/channel.go rename to internal/discord/session/channel/messenger/sender/completer/channel.go index f378484..1d603af 100644 --- a/internal/_discord/channel/message/send/complete/channel.go +++ b/internal/discord/session/channel/messenger/sender/completer/channel.go @@ -1,10 +1,10 @@ -package complete +package completer import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat/text" ) @@ -40,7 +40,7 @@ func DMChannels(s *state.Instance, word string) []cchat.CompletionEntry { func rankChannel(word string, ch discord.Channel) int { switch ch.Type { case discord.GroupDM, discord.DirectMessage: - return rankFunc(word, ch.Name+" "+shared.ChannelName(ch)) + return rankFunc(word, ch.Name+" "+mention.ChannelName(ch)) default: return rankFunc(word, ch.Name) } diff --git a/internal/_discord/channel/message/send/complete/completer.go b/internal/discord/session/channel/messenger/sender/completer/completer.go similarity index 95% rename from internal/_discord/channel/message/send/complete/completer.go rename to internal/discord/session/channel/messenger/sender/completer/completer.go index 7a2495e..ba8d470 100644 --- a/internal/_discord/channel/message/send/complete/completer.go +++ b/internal/discord/session/channel/messenger/sender/completer/completer.go @@ -1,10 +1,10 @@ -package complete +package completer import ( "sort" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/lithammer/fuzzysearch/fuzzy" ) diff --git a/internal/_discord/channel/message/send/complete/emoji.go b/internal/discord/session/channel/messenger/sender/completer/emoji.go similarity index 98% rename from internal/_discord/channel/message/send/complete/emoji.go rename to internal/discord/session/channel/messenger/sender/completer/emoji.go index f67bfa8..c11bcce 100644 --- a/internal/_discord/channel/message/send/complete/emoji.go +++ b/internal/discord/session/channel/messenger/sender/completer/emoji.go @@ -1,4 +1,4 @@ -package complete +package completer import ( "github.com/diamondburned/arikawa/v2/discord" diff --git a/internal/_discord/channel/message/send/complete/mention.go b/internal/discord/session/channel/messenger/sender/completer/mention.go similarity index 98% rename from internal/_discord/channel/message/send/complete/mention.go rename to internal/discord/session/channel/messenger/sender/completer/mention.go index e535a86..4290011 100644 --- a/internal/_discord/channel/message/send/complete/mention.go +++ b/internal/discord/session/channel/messenger/sender/completer/mention.go @@ -1,4 +1,4 @@ -package complete +package completer import ( "github.com/diamondburned/arikawa/v2/discord" @@ -51,7 +51,7 @@ func GuildMessageMentions( entries = append(entries, cchat.CompletionEntry{ Raw: msg.Author.Mention(), - Text: message.RenderAuthorName(user), + Text: message.RenderUserName(user), Secondary: text.Plain(msg.Author.Username + "#" + msg.Author.Discriminator), IconURL: msg.Author.AvatarURL(), }) @@ -252,7 +252,7 @@ func (ch ChannelCompleter) CompleteMentions(word string) []cchat.CompletionEntry entries = append(entries, cchat.CompletionEntry{ Raw: raw, - Text: message.RenderAuthorName(user), + Text: message.RenderUserName(user), Secondary: text.Plain(m.User.Username + "#" + m.User.Discriminator), IconURL: user.Avatar(), }) diff --git a/internal/_discord/channel/message/send/sender.go b/internal/discord/session/channel/messenger/sender/sender.go similarity index 70% rename from internal/_discord/channel/message/send/sender.go rename to internal/discord/session/channel/messenger/sender/sender.go index e6c41a3..068315b 100644 --- a/internal/_discord/channel/message/send/sender.go +++ b/internal/discord/session/channel/messenger/sender/sender.go @@ -1,4 +1,4 @@ -package send +package sender import ( "github.com/diamondburned/arikawa/v2/api" @@ -6,21 +6,22 @@ import ( "github.com/diamondburned/arikawa/v2/utils/json/option" "github.com/diamondburned/arikawa/v2/utils/sendpart" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" - "github.com/diamondburned/cchat-discord/internal/discord/channel/shared" - "github.com/diamondburned/cchat-discord/internal/discord/config" + "github.com/diamondburned/cchat-discord/internal/config" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/sender/completer" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/state" ) -var ( - allowAllMention = []api.AllowedMentionType{ - api.AllowEveryoneMention, - api.AllowRoleMention, - api.AllowUserMention, - } -) +var allowAllMention = []api.AllowedMentionType{ + api.AllowEveryoneMention, + api.AllowRoleMention, + api.AllowUserMention, +} + +// WrapMessage wraps the given msg to return a new SendMessageData. +func WrapMessage( + s *state.Instance, ch discord.ChannelID, msg cchat.SendableMessage) api.SendMessageData { -func WrapMessage(s *state.Instance, msg cchat.SendableMessage) api.SendMessageData { var send = api.SendMessageData{ Content: msg.Content(), } @@ -47,7 +48,8 @@ func WrapMessage(s *state.Instance, msg cchat.SendableMessage) api.SendMessageDa RepliedUser: option.False, } - if config.MentionOnReply() { + repTo, err := s.Cabinet.Message(ch, discord.MessageID(id)) + if err == nil && config.MentionOnReply(repTo.ID.Time()) { send.AllowedMentions.RepliedUser = option.True } } @@ -66,7 +68,7 @@ func New(ch shared.Channel) Sender { } func (s Sender) Send(msg cchat.SendableMessage) error { - _, err := s.State.SendMessageComplex(s.ID, WrapMessage(s.State, msg)) + _, err := s.State.SendMessageComplex(s.ID, WrapMessage(s.State, s.ID, msg)) return err } @@ -76,7 +78,7 @@ func (s Sender) CanAttach() bool { } func (s Sender) AsCompleter() cchat.Completer { - return complete.New(s.Channel) + return completer.New(s.Channel) } func addAttachments(atts []cchat.MessageAttachment) []sendpart.File { diff --git a/internal/_discord/channel/shared/channel.go b/internal/discord/session/channel/shared/channel.go similarity index 100% rename from internal/_discord/channel/shared/channel.go rename to internal/discord/session/channel/shared/channel.go diff --git a/internal/discord/folder/folder.go b/internal/discord/session/guild/folder/folder.go similarity index 92% rename from internal/discord/folder/folder.go rename to internal/discord/session/guild/folder/folder.go index a8c7035..6d33391 100644 --- a/internal/discord/folder/folder.go +++ b/internal/discord/session/guild/folder/folder.go @@ -7,8 +7,8 @@ import ( "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/guild" - "github.com/diamondburned/cchat-discord/internal/discord/shared/state" + "github.com/diamondburned/cchat-discord/internal/discord/session/guild" + "github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/segments/colored" "github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/utils/empty" diff --git a/internal/discord/guild/guild.go b/internal/discord/session/guild/guild.go similarity index 87% rename from internal/discord/guild/guild.go rename to internal/discord/session/guild/guild.go index 6982fe8..0d18880 100644 --- a/internal/discord/guild/guild.go +++ b/internal/discord/session/guild/guild.go @@ -6,10 +6,10 @@ import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/category" - "github.com/diamondburned/cchat-discord/internal/discord/channel" - "github.com/diamondburned/cchat-discord/internal/discord/shared/funcutil" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/category" "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat-discord/internal/funcutil" "github.com/diamondburned/cchat/utils/empty" "github.com/pkg/errors" ) @@ -45,7 +45,7 @@ func (g *Guild) ID() cchat.ID { } func (g *Guild) Name(ctx context.Context, l cchat.LabelContainer) (func(), error) { - return g.state.Labels.AddGuildLabel(g.id, l) + return g.state.Labels.AddGuildLabel(g.id, l), nil } func (g *Guild) AsLister() cchat.Lister { return g } @@ -53,7 +53,7 @@ func (g *Guild) AsLister() cchat.Lister { return g } func (g *Guild) Servers(container cchat.ServersContainer) (func(), error) { c, err := g.state.Channels(g.id) if err != nil { - return errors.Wrap(err, "Failed to get channels") + return nil, errors.Wrap(err, "Failed to get channels") } // Only get top-level channels (those with category ID being null). @@ -89,13 +89,11 @@ func (g *Guild) Servers(container cchat.ServersContainer) (func(), error) { container.SetServers(chs) + // TODO: account for insertion/deletion. // TODO: RACEEEEEEEEEEEEEEEEEEEEEEE CONDITION!!!!!!!!!!!! - // TODO: Add channel stuff. stop := funcutil.JoinCancels() - // TODO: account for insertion/deletion. - return stop, nil } diff --git a/internal/_discord/private/hub/messages.go b/internal/discord/session/private/hub/messages.go similarity index 92% rename from internal/_discord/private/hub/messages.go rename to internal/discord/session/private/hub/messages.go index 1788fe9..8b3d03b 100644 --- a/internal/_discord/private/hub/messages.go +++ b/internal/discord/session/private/hub/messages.go @@ -7,8 +7,8 @@ import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" "github.com/diamondburned/cchat-discord/internal/discord/message" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/sender/completer" "github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/discord/state/nonce" "github.com/diamondburned/cchat-discord/internal/funcutil" @@ -83,21 +83,21 @@ func NewMessages(s *state.Instance, acList *activeList, adder ChannelAdder) *Mes messages: make(messageList, 0, 100), } - hubServer.sender.completers.Prefixes = complete.CompleterPrefixes{ + hubServer.sender.completers.Prefixes = completer.CompleterPrefixes{ ':': func(word string) []cchat.CompletionEntry { - return complete.Emojis(s, 0, word) + return completer.Emojis(s, 0, word) }, '@': func(word string) []cchat.CompletionEntry { if word != "" { - return complete.AllUsers(s, word) + return completer.AllUsers(s, word) } hubServer.msgMutex.Lock() defer hubServer.msgMutex.Unlock() - return complete.MessageMentions(hubServer.messages) + return completer.MessageMentions(hubServer.messages) }, '#': func(word string) []cchat.CompletionEntry { - return complete.DMChannels(s, word) + return completer.DMChannels(s, word) }, } @@ -170,7 +170,7 @@ func (msgs *Messages) JoinServer(ctx context.Context, ct cchat.MessagesContainer user := mention.NewUser(msg.Author) user.WithState(msgs.state.State) - var author = message.NewAuthor(user) + var author = message.NewAuthor(msgs.state, user) if isReply { c, err := msgs.state.Channel(msg.ChannelID) if err == nil { diff --git a/internal/_discord/private/hub/sender.go b/internal/discord/session/private/hub/sender.go similarity index 89% rename from internal/_discord/private/hub/sender.go rename to internal/discord/session/private/hub/sender.go index 27ede8b..ac0d28e 100644 --- a/internal/_discord/private/hub/sender.go +++ b/internal/discord/session/private/hub/sender.go @@ -6,8 +6,8 @@ import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/send" - "github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/sender" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/sender/completer" "github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/discord/state/nonce" "github.com/diamondburned/cchat/utils/empty" @@ -28,7 +28,7 @@ type Sender struct { sentMsgs *nonce.Set state *state.Instance - completers complete.Completer + completers completer.Completer } // mentionRegex matche the following: @@ -89,7 +89,7 @@ func (s *Sender) Send(sendable cchat.SendableMessage) error { s.adder.AddChannel(s.state, channel) } - sendData := send.WrapMessage(s.state, sendable) + sendData := sender.WrapMessage(s.state, channel.ID, sendable) sendData.Content = strings.TrimPrefix(content, matches[0]) // Store the nonce. diff --git a/internal/_discord/private/hub/server.go b/internal/discord/session/private/hub/server.go similarity index 95% rename from internal/_discord/private/hub/server.go rename to internal/discord/session/private/hub/server.go index ddae0eb..4b71728 100644 --- a/internal/_discord/private/hub/server.go +++ b/internal/discord/session/private/hub/server.go @@ -1,6 +1,7 @@ package hub import ( + "context" "sync" "time" @@ -137,7 +138,10 @@ func New(s *state.Instance, adder ChannelAdder) (*Server, error) { func (hub *Server) ID() cchat.ID { return "!!!hub-server!!!" } -func (hub *Server) Name() text.Rich { return text.Plain("Incoming Messages") } +func (hub *Server) Name(_ context.Context, l cchat.LabelContainer) (func(), error) { + l.SetLabel(text.Plain("Incoming Messages")) + return func() {}, nil +} // ActiveChannelIDs returns the list of active channel IDs, that is, the channel // IDs that should be displayed separately. diff --git a/internal/_discord/private/private.go b/internal/discord/session/private/private.go similarity index 86% rename from internal/_discord/private/private.go rename to internal/discord/session/private/private.go index ba3eaa0..4c4f67d 100644 --- a/internal/_discord/private/private.go +++ b/internal/discord/session/private/private.go @@ -1,14 +1,15 @@ package private import ( + "context" "sort" "sync" "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/channel" - "github.com/diamondburned/cchat-discord/internal/discord/private/hub" + "github.com/diamondburned/cchat-discord/internal/discord/session/channel" + "github.com/diamondburned/cchat-discord/internal/discord/session/private/hub" "github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/utils/empty" @@ -90,8 +91,9 @@ func (priv Private) ID() cchat.ID { return "!!!private-container!!!" } -func (priv Private) Name() text.Rich { - return text.Plain("Private Channels") +func (priv Private) Name(_ context.Context, l cchat.LabelContainer) (func(), error) { + l.SetLabel(text.Plain("Private Channels")) + return func() {}, nil } func (priv Private) AsLister() cchat.Lister { return priv } @@ -115,7 +117,7 @@ func (active activeChannel) LastMessageID() discord.MessageID { return discord.MessageID(active.Channel.ID) } -func (priv Private) Servers(container cchat.ServersContainer) error { +func (priv Private) Servers(container cchat.ServersContainer) (func(), error) { activeIDs := priv.hub.ActiveChannelIDs() channels := make([]activeChannel, 0, len(activeIDs)) @@ -123,7 +125,7 @@ func (priv Private) Servers(container cchat.ServersContainer) error { for _, id := range activeIDs { c, err := priv.state.Channel(id) if err != nil { - return errors.Wrap(err, "failed to get private channel") + return nil, errors.Wrap(err, "failed to get private channel") } channels = append(channels, activeChannel{ @@ -144,7 +146,7 @@ func (priv Private) Servers(container cchat.ServersContainer) error { for i, ch := range channels { c, err := channel.New(priv.state, *ch.Channel) if err != nil { - return errors.Wrap(err, "failed to create server for private channel") + return nil, errors.Wrap(err, "failed to create server for private channel") } servers[i+1] = c @@ -152,5 +154,5 @@ func (priv Private) Servers(container cchat.ServersContainer) error { container.SetServers(servers) priv.containers.Register(container) - return nil + return func() {}, nil } diff --git a/internal/_discord/session/restorer.go b/internal/discord/session/restorer.go similarity index 100% rename from internal/_discord/session/restorer.go rename to internal/discord/session/restorer.go diff --git a/internal/discord/session/session.go b/internal/discord/session/session.go index 2d065ff..b02719c 100644 --- a/internal/discord/session/session.go +++ b/internal/discord/session/session.go @@ -7,13 +7,11 @@ import ( "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/session" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/folder" - "github.com/diamondburned/cchat-discord/internal/discord/guild" - "github.com/diamondburned/cchat-discord/internal/discord/private" - "github.com/diamondburned/cchat-discord/internal/discord/shared/state" + "github.com/diamondburned/cchat-discord/internal/discord/session/guild" + "github.com/diamondburned/cchat-discord/internal/discord/session/guild/folder" + "github.com/diamondburned/cchat-discord/internal/discord/session/private" + "github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/funcutil" - "github.com/diamondburned/cchat-discord/internal/segments/mention" - "github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/ningen/v2" "github.com/pkg/errors" @@ -44,27 +42,7 @@ func (s *Session) ID() cchat.ID { } func (s *Session) Name(ctx context.Context, l cchat.LabelContainer) (func(), error) { - u, err := s.state.Cabinet.Me() - if err != nil { - l.SetLabel(text.Plain("<@" + s.state.UserID.String() + ">")) - } else { - user := mention.NewUser(*u) - user.WithState(s.state.State) - user.Prefetch() - - rich := text.Plain(user.DisplayName()) - rich.Segments = []text.Segment{ - mention.Segment{ - End: len(rich.Content), - User: user, - }, - } - - l.SetLabel(rich) - } - - // TODO. - return func() {}, nil + return s.state.Labels.AddPresenceLabel(s.state.UserID, l), nil } func (s *Session) Disconnect() error { diff --git a/internal/discord/shared/state/nonce/nonce.go b/internal/discord/shared/state/nonce/nonce.go deleted file mode 100644 index 898c496..0000000 --- a/internal/discord/shared/state/nonce/nonce.go +++ /dev/null @@ -1,90 +0,0 @@ -package nonce - -import ( - "encoding/base64" - "encoding/binary" - "fmt" - "strconv" - "sync" - "sync/atomic" - "time" - - cryptorand "crypto/rand" - mathrand "math/rand" -) - -func init() { - mathrand.Seed(time.Now().UnixNano()) -} - -var nonceCounter uint64 - -// generateNonce generates a unique nonce ID. -func generateNonce() string { - return fmt.Sprintf( - "%s-%s-%s", - strconv.FormatInt(time.Now().Unix(), 36), - randomBits(), - strconv.FormatUint(atomic.AddUint64(&nonceCounter, 1), 36), - ) -} - -// randomBits returns a string 6 bytes long with random characters that are safe -// to print. It falls back to math/rand's pseudorandom number generator if it -// cannot read from the system entropy pool. -func randomBits() string { - randBits := make([]byte, 2) - - _, err := cryptorand.Read(randBits) - if err != nil { - binary.LittleEndian.PutUint32(randBits, mathrand.Uint32()) - } - - return base64.RawStdEncoding.EncodeToString(randBits) -} - -// Map is a nonce state that keeps track of known nonces and generates a -// Discord-compatible nonce string. -type Map sync.Map - -// Generate generates a new internal nonce, add a bind from the new nonce to the -// original nonce, then return the new nonce. If the given original nonce is -// empty, then an empty string is returned. -func (nmap *Map) Generate(original string) string { - // Ignore empty nonces. - if original == "" { - return "" - } - - newNonce := generateNonce() - (*sync.Map)(nmap).Store(newNonce, original) - return newNonce -} - -// Load grabs the nonce and permanently deleting it if the given nonce is found. -func (nmap *Map) Load(newNonce string) string { - v, ok := (*sync.Map)(nmap).LoadAndDelete(newNonce) - if ok { - return v.(string) - } - return "" -} - -// Set is a unique set of nonces. -type Set sync.Map - -var nonceSentinel = struct{}{} - -func (nset *Set) Store(nonce string) { - (*sync.Map)(nset).Store(nonce, nonceSentinel) -} - -func (nset *Set) Has(nonce string) bool { - _, ok := (*sync.Map)(nset).Load(nonce) - return ok -} - -func (nset *Set) HasAndDelete(nonce string) bool { - _, ok := (*sync.Map)(nset).LoadAndDelete(nonce) - return ok -} diff --git a/internal/discord/shared/state/labels/container.go b/internal/discord/state/labels/container.go similarity index 77% rename from internal/discord/shared/state/labels/container.go rename to internal/discord/state/labels/container.go index 17843b8..95b11ff 100644 --- a/internal/discord/shared/state/labels/container.go +++ b/internal/discord/state/labels/container.go @@ -6,16 +6,16 @@ import ( ) type labelContainers struct { - guilds map[discord.GuildID]guildContainer - channels map[discord.ChannelID]labelerList - // presences map[discord.UserID]labelerList + guilds map[discord.GuildID]guildContainer + channels map[discord.ChannelID]labelerList + presences map[discord.UserID]labelerList } func newLabelContainers() labelContainers { return labelContainers{ - guilds: map[discord.GuildID]guildContainer{}, - channels: map[discord.ChannelID]labelerList{}, - // presences: map[discord.UserID]labelerList{}, + guilds: map[discord.GuildID]guildContainer{}, + channels: map[discord.ChannelID]labelerList{}, + presences: map[discord.UserID]labelerList{}, } } diff --git a/internal/discord/shared/state/labels/labels.go b/internal/discord/state/labels/labels.go similarity index 81% rename from internal/discord/shared/state/labels/labels.go rename to internal/discord/state/labels/labels.go index da386ec..3b8ccc4 100644 --- a/internal/discord/shared/state/labels/labels.go +++ b/internal/discord/state/labels/labels.go @@ -6,6 +6,7 @@ import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/funcutil" "github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/ningen/v2" ) @@ -17,7 +18,7 @@ import ( // adder function will do nothing and will return a callback that does nothing. type Repository struct { state *ningen.State - detachs []func() + detach func() stopped bool mutex sync.Mutex @@ -31,13 +32,15 @@ func NewRepository(state *ningen.State) *Repository { stores: newLabelContainers(), } - r.detachs = []func(){ + r.detach = funcutil.JoinCancels( state.AddHandler(r.onGuildUpdate), state.AddHandler(r.onMemberUpdate), state.AddHandler(r.onMemberRemove), state.AddHandler(r.onChannelUpdate), state.AddHandler(r.onChannelDelete), - } + state.AddHandler(r.onGuildMembersChunk), + // TODO: *gateway.GuildMemberListUpdate + ) return &r } @@ -115,11 +118,41 @@ func (r *Repository) onMemberUpdate(ev *gateway.GuildMemberUpdateEvent) { } } -// AddMemberLabel adds a label to display the given member live. Refer to +func (r *Repository) onGuildMembersChunk(chunk *gateway.GuildMembersChunkEvent) { + r.mutex.Lock() + defer r.mutex.Unlock() + + if r.stopped { + return + } + + guild, ok := r.stores.guilds[chunk.GuildID] + if !ok { + return + } + + for _, member := range chunk.Members { + m, ok := guild.members[member.User.ID] + if ok { + rich := mention.NewMemberText(r.state, chunk.GuildID, member.User.ID) + + for labeler := range m { + labeler.SetLabel(rich) + } + } + } +} + +// AddMemberLabel adds a label to display the given member live. If the given +// guildID is not valid, then AddPresenceLabel will be called. Refer to // Repository for more documentation. func (r *Repository) AddMemberLabel( guildID discord.GuildID, userID discord.UserID, l cchat.LabelContainer) func() { + if !guildID.IsValid() { + return r.AddPresenceLabel(userID, l) + } + l.SetLabel(mention.NewMemberText(r.state, guildID, userID)) r.mutex.Lock() @@ -208,14 +241,18 @@ func (r *Repository) AddChannelLabel(chID discord.ChannelID, l cchat.LabelContai } } +func (r *Repository) AddPresenceLabel(uID discord.UserID, l cchat.LabelContainer) func() { + // TODO: Presence update events + // TODO: user fallbacks + panic("Implement me") + return nil +} + // Stop detaches all handlers. func (r *Repository) Stop() { r.mutex.Lock() - defer r.mutex.Unlock() - r.stopped = true + r.mutex.Unlock() - for _, detach := range r.detachs { - detach() - } + r.detach() } diff --git a/internal/discord/shared/state/labels/labels_wip.go b/internal/discord/state/labels/labels_wip.go similarity index 100% rename from internal/discord/shared/state/labels/labels_wip.go rename to internal/discord/state/labels/labels_wip.go diff --git a/internal/_discord/state/nonce/nonce.go b/internal/discord/state/nonce/nonce.go similarity index 100% rename from internal/_discord/state/nonce/nonce.go rename to internal/discord/state/nonce/nonce.go diff --git a/internal/discord/shared/state/state.go b/internal/discord/state/state.go similarity index 97% rename from internal/discord/shared/state/state.go rename to internal/discord/state/state.go index e79530c..6310392 100644 --- a/internal/discord/shared/state/state.go +++ b/internal/discord/state/state.go @@ -5,11 +5,11 @@ import ( "context" "log" - "github.com/diamondburned/arikawa/utils/httputil/httpdriver" "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/session" "github.com/diamondburned/arikawa/v2/state" "github.com/diamondburned/arikawa/v2/state/store/defaultstore" + "github.com/diamondburned/arikawa/v2/utils/httputil/httpdriver" "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-discord/internal/discord/state/labels" "github.com/diamondburned/cchat-discord/internal/discord/state/nonce" diff --git a/internal/discord/shared/funcutil/funcutil.go b/internal/funcutil/funcutil.go similarity index 100% rename from internal/discord/shared/funcutil/funcutil.go rename to internal/funcutil/funcutil.go diff --git a/internal/segments/mention/channel.go b/internal/segments/mention/channel.go index 4514c61..dd6fca0 100644 --- a/internal/segments/mention/channel.go +++ b/internal/segments/mention/channel.go @@ -63,14 +63,20 @@ func NewChannelText(s *ningen.State, chID discord.ChannelID) text.Rich { } rich := text.Rich{Content: ChannelName(*ch)} - rich.Segments = []text.Segment{ - Segment{ - Start: 0, - End: len(rich.Content), - Channel: NewChannel(*ch), - }, + segment := Segment{ + Start: 0, + End: len(rich.Content), } + if ch.Type == discord.DirectMessage && len(ch.DMRecipients) == 1 { + segment.User = NewUser(ch.DMRecipients[0]) + segment.User.WithState(s) + segment.User.Prefetch() + } else { + segment.Channel = NewChannel(*ch) + } + + rich.Segments = []text.Segment{segment} return rich } diff --git a/internal/segments/mention/user.go b/internal/segments/mention/user.go index e4c5364..6e0a6df 100644 --- a/internal/segments/mention/user.go +++ b/internal/segments/mention/user.go @@ -91,6 +91,11 @@ func (um *User) UserID() discord.UserID { return um.user.ID } +// GuildID returns the guild ID, if any. +func (um *User) GuildID() discord.GuildID { + return um.guildID +} + // SetGuildID sets the user's guild ID. func (um *User) WithGuildID(guildID discord.GuildID) { um.guildID = guildID