Partial rewrite for cchat v0.5.1

This commit is contained in:
diamondburned 2021-03-14 21:30:05 -07:00
parent 7bfe466482
commit 36886b0cad
57 changed files with 446 additions and 1054 deletions

View File

@ -1,9 +1,11 @@
package discord package discord
import ( import (
"context"
"github.com/diamondburned/cchat" "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/authenticate"
"github.com/diamondburned/cchat-discord/internal/discord/config"
"github.com/diamondburned/cchat-discord/internal/discord/session" "github.com/diamondburned/cchat-discord/internal/discord/session"
"github.com/diamondburned/cchat-discord/internal/segments/avatar" "github.com/diamondburned/cchat-discord/internal/segments/avatar"
"github.com/diamondburned/cchat/services" "github.com/diamondburned/cchat/services"
@ -29,11 +31,13 @@ type Service struct {
empty.Service empty.Service
} }
func (Service) Name() text.Rich { func (Service) Name(_ context.Context, l cchat.LabelContainer) (func(), error) {
return text.Rich{ l.SetLabel(text.Rich{
Content: "Discord", Content: "Discord",
Segments: []text.Segment{Logo}, Segments: []text.Segment{Logo},
} })
return func() {}, nil
} }
func (Service) Authenticate() []cchat.Authenticator { func (Service) Authenticate() []cchat.Authenticator {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,21 +9,9 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
var World = &registry{ type customType interface {
configs: []config{ Marshal() string
{"Mention on Reply", true}, Unmarshal(string) error
{"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 config struct { type config struct {
@ -55,13 +43,14 @@ func (c *config) Unmarshal(src map[string]string) (err error) {
} }
} }
var v interface{} switch v := c.Value.(type) {
switch c.Value.(type) {
case bool: case bool:
v, err = strconv.ParseBool(strVal) c.Value, err = strconv.ParseBool(strVal)
case string: case string:
v = strVal c.Value = strVal
case customType:
err = v.Unmarshal(strVal)
c.Value = v
default: default:
err = fmt.Errorf("unknown type %T", c.Value) 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 return nil
} }

32
internal/config/world.go Normal file
View File

@ -0,0 +1,32 @@
package config
import (
"time"
"github.com/diamondburned/cchat"
)
var World cchat.Configurator = world
var world = &registry{
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)
}

View File

@ -1,9 +1,10 @@
package message package message
import ( import (
"context"
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "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/discord/state"
"github.com/diamondburned/cchat-discord/internal/segments/colored" "github.com/diamondburned/cchat-discord/internal/segments/colored"
"github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat-discord/internal/segments/mention"
@ -13,22 +14,26 @@ import (
) )
type Author struct { type Author struct {
name text.Rich name text.Rich
user *mention.User // same pointer as in name user *mention.User // same pointer as in name
state *state.Instance
} }
var _ cchat.User = (*Author)(nil) var _ cchat.User = (*Author)(nil)
// NewAuthor creates a new message author. // 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{ return Author{
name: RenderAuthorName(user), name: RenderUserName(user),
user: user, user: user,
state: s,
} }
} }
// RenderAuthorName renders the given user mention into a text segment. // RenderUserName renders the given user mention into a text segment.
func RenderAuthorName(user *mention.User) text.Rich { func RenderUserName(user *mention.User) text.Rich {
var rich text.Rich var rich text.Rich
richUser(&rich, user) richUser(&rich, user)
return rich 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 return
} }
@ -55,17 +64,18 @@ func (a Author) ID() cchat.ID {
return a.user.UserID().String() return a.user.UserID().String()
} }
func (a Author) Name() text.Rich { // Name subscribes the author to the global name label registry.
return a.name 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.state.Labels.AddPresenceLabel(a.user.UserID(), l), nil
return a.user.Avatar()
} }
const authorReplyingTo = " replying to " 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. // Specifically, this function is used for direct messages in virtual channels.
func (a *Author) AddUserReply(user discord.User, s *state.Instance) { func (a *Author) AddUserReply(user discord.User, s *state.Instance) {
a.name.Content += authorReplyingTo a.name.Content += authorReplyingTo
@ -87,7 +97,7 @@ func (a *Author) AddChannelReply(ch discord.Channel, s *state.Instance) {
} }
a.name.Content += authorReplyingTo 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, a.name.Segments = append(a.name.Segments,
mention.Segment{ 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) { func (a *Author) AddMessageReference(ref discord.Message, s *state.Instance) {
a.name.Content += authorReplyingTo a.name.Content += authorReplyingTo

View File

@ -100,7 +100,7 @@ func NewGuildMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Mes
user.Prefetch() 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 // 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.WithState(s.State)
user.Prefetch() user.Prefetch()
return NewMessage(m, s, NewAuthor(user)) return NewMessage(m, s, NewAuthor(s, user))
} }
// NewDirectMessage creates a new direct message. // NewDirectMessage creates a new direct message.
@ -127,7 +127,7 @@ func NewDirectMessage(m discord.Message, s *state.Instance) Message {
user.WithState(s.State) user.WithState(s.State)
user.Prefetch() 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. // 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.WithGuildID(msg.GuildID)
user.WithMember(m) user.WithMember(m)
author := NewAuthor(user) author := NewAuthor(s, user)
if ref := ReferencedMessage(msg, s, true); ref != nil { if ref := ReferencedMessage(msg, s, true); ref != nil {
author.AddMessageReference(*ref, s) 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 { if m.author.user == nil {
return nil return nil
} }
@ -374,7 +374,11 @@ func segmentFuncFromMention(m discord.Message, s *state.Instance) func(i, j int)
user.Prefetch() user.Prefetch()
return mention.NewSegment(i, j, user) return mention.Segment{
Start: i,
End: j,
User: user,
}
} }
} }

View File

@ -1,13 +1,13 @@
package category package category
import ( import (
"context"
"sort" "sort"
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "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-discord/internal/discord/state"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/cchat/utils/empty"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -76,24 +76,16 @@ func (c *Category) ID() cchat.ID {
return c.id.String() return c.id.String()
} }
func (c *Category) Name() text.Rich { func (c *Category) Name(_ context.Context, l cchat.LabelContainer) (func(), error) {
t, err := c.state.Channel(c.id) return c.state.Labels.AddChannelLabel(c.id, l), nil
if err != nil {
// This shouldn't happen.
return text.Rich{Content: c.id.String()}
}
return text.Rich{
Content: t.Name,
}
} }
func (c *Category) AsLister() cchat.Lister { return c } 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) t, err := c.state.Channels(c.guildID)
if err != nil { 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. // Filter out channels with this category ID.
@ -107,12 +99,12 @@ func (c *Category) Servers(container cchat.ServersContainer) error {
for i := range chs { for i := range chs {
c, err := channel.New(c.state, chs[i]) c, err := channel.New(c.state, chs[i])
if err != nil { 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 chv[i] = c
} }
container.SetServers(chv) container.SetServers(chv)
return nil return func() {}, nil
} }

View File

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

View File

@ -4,21 +4,21 @@ import (
"strings" "strings"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/channel/commands" "github.com/diamondburned/cchat-discord/internal/discord/session/channel/commands"
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/sender/completer"
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
) )
type Commander struct { type Commander struct {
shared.Channel shared.Channel
msgCompl complete.ChannelCompleter msgCompl completer.ChannelCompleter
} }
func NewCommander(ch shared.Channel) cchat.Commander { func NewCommander(ch shared.Channel) cchat.Commander {
return Commander{ return Commander{
Channel: ch, Channel: ch,
msgCompl: complete.ChannelCompleter{ msgCompl: completer.ChannelCompleter{
Channel: ch, Channel: ch,
}, },
} }

View File

@ -3,7 +3,7 @@ package commands
import ( import (
"bytes" "bytes"
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared"
) )
type Command struct { type Command struct {

View File

@ -10,7 +10,7 @@ import (
"github.com/diamondburned/arikawa/v2/bot/extras/arguments" "github.com/diamondburned/arikawa/v2/bot/extras/arguments"
"github.com/diamondburned/arikawa/v2/discord" "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" "github.com/pkg/errors"
) )

View File

@ -1,9 +1,9 @@
package action package actioner
import ( import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "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" "github.com/pkg/errors"
) )

View File

@ -1,12 +1,12 @@
package backlog package backlogger
import ( import (
"context" "context"
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "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/message"
"github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared"
"github.com/pkg/errors" "github.com/pkg/errors"
) )

View File

@ -1,9 +1,9 @@
package edit package editor
import ( import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "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" "github.com/pkg/errors"
) )

View File

@ -1,26 +1,53 @@
package typer package indicator
import ( import (
"time" "time"
"github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "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/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/discord/state"
"github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type Typer struct { type TypingIndicator struct {
message.Author shared.Channel
time discord.UnixTimestamp
} }
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. // 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 var user *mention.User
if ev.GuildID.IsValid() { if ev.GuildID.IsValid() {
@ -56,12 +83,5 @@ func New(s *state.Instance, ev *gateway.TypingStartEvent) (*Typer, error) {
user.WithState(s.State) user.WithState(s.State)
user.Prefetch() user.Prefetch()
return &Typer{ return message.NewAuthor(s, user), nil
Author: message.NewAuthor(user),
time: ev.Timestamp,
}, nil
}
func (t Typer) Time() time.Time {
return t.time.Time()
} }

View File

@ -1,9 +1,9 @@
package indicate package indicator
import ( import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "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/diamondburned/ningen/v2/states/read"
"github.com/pkg/errors" "github.com/pkg/errors"
) )

View File

@ -1,4 +1,4 @@
package memberlist package memberlister
import ( import (
"context" "context"
@ -8,7 +8,7 @@ import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "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/emoji"
"github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
@ -40,18 +40,21 @@ func (l *Member) ID() cchat.ID {
return l.mention.UserID().String() 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() l.mention.Prefetch()
content := l.mention.DisplayName() content := l.mention.DisplayName()
labeler.SetLabel(text.Rich{ labeler.SetLabel(text.Rich{
Content: content, Content: content,
Segments: []text.Segment{ 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 { func (l *Member) Status() cchat.Status {

View File

@ -1,11 +1,11 @@
package memberlist package memberlister
import ( import (
"context" "context"
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "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" "github.com/diamondburned/ningen/v2/states/member"
) )

View File

@ -1,4 +1,4 @@
package memberlist package memberlister
import ( import (
"context" "context"
@ -7,7 +7,7 @@ import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "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" "github.com/diamondburned/cchat/text"
) )
@ -61,9 +61,9 @@ func (s Section) ID() cchat.ID {
return s.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)) labeler.SetLabel(text.Plain(s.name))
return nil return func() {}, nil
} }
func (s Section) Total() int { func (s Section) Total() int {

View File

@ -1,4 +1,4 @@
package message package messenger
import ( import (
"context" "context"
@ -7,15 +7,14 @@ import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "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/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-discord/internal/funcutil"
"github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/cchat/utils/empty"
) )
@ -107,7 +106,7 @@ func (msgr *Messenger) AsSender() cchat.Sender {
return nil return nil
} }
return send.New(msgr.Channel) return sender.New(msgr.Channel)
} }
func (msgr *Messenger) AsEditor() cchat.Editor { func (msgr *Messenger) AsEditor() cchat.Editor {
@ -115,22 +114,22 @@ func (msgr *Messenger) AsEditor() cchat.Editor {
return nil return nil
} }
return edit.New(msgr.Channel) return editor.New(msgr.Channel)
} }
func (msgr *Messenger) AsActioner() cchat.Actioner { func (msgr *Messenger) AsActioner() cchat.Actioner {
return action.New(msgr.Channel) return actioner.New(msgr.Channel)
} }
func (msgr *Messenger) AsNicknamer() cchat.Nicknamer { func (msgr *Messenger) AsNicknamer() cchat.Nicknamer {
return nickname.New(msgr.Channel) return NewMeNicknamer(msgr.Channel)
} }
func (msgr *Messenger) AsMemberLister() cchat.MemberLister { func (msgr *Messenger) AsMemberLister() cchat.MemberLister {
if !msgr.GuildID.IsValid() { if !msgr.GuildID.IsValid() {
return nil return nil
} }
return memberlist.New(msgr.Channel) return memberlister.New(msgr.Channel)
} }
func (msgr *Messenger) AsBacklogger() cchat.Backlogger { func (msgr *Messenger) AsBacklogger() cchat.Backlogger {
@ -138,13 +137,13 @@ func (msgr *Messenger) AsBacklogger() cchat.Backlogger {
return nil return nil
} }
return backlog.New(msgr.Channel) return backlogger.New(msgr.Channel)
} }
func (msgr *Messenger) AsTypingIndicator() cchat.TypingIndicator { func (msgr *Messenger) AsTypingIndicator() cchat.TypingIndicator {
return indicate.NewTyping(msgr.Channel) return indicator.NewTyping(msgr.Channel)
} }
func (msgr *Messenger) AsUnreadIndicator() cchat.UnreadIndicator { func (msgr *Messenger) AsUnreadIndicator() cchat.UnreadIndicator {
return indicate.NewUnread(msgr.Channel) return indicator.NewUnread(msgr.Channel)
} }

View File

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

View File

@ -1,10 +1,10 @@
package complete package completer
import ( import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "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/discord/state"
"github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/diamondburned/cchat/text" "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 { func rankChannel(word string, ch discord.Channel) int {
switch ch.Type { switch ch.Type {
case discord.GroupDM, discord.DirectMessage: case discord.GroupDM, discord.DirectMessage:
return rankFunc(word, ch.Name+" "+shared.ChannelName(ch)) return rankFunc(word, ch.Name+" "+mention.ChannelName(ch))
default: default:
return rankFunc(word, ch.Name) return rankFunc(word, ch.Name)
} }

View File

@ -1,10 +1,10 @@
package complete package completer
import ( import (
"sort" "sort"
"github.com/diamondburned/cchat" "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" "github.com/lithammer/fuzzysearch/fuzzy"
) )

View File

@ -1,4 +1,4 @@
package complete package completer
import ( import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"

View File

@ -1,4 +1,4 @@
package complete package completer
import ( import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
@ -51,7 +51,7 @@ func GuildMessageMentions(
entries = append(entries, cchat.CompletionEntry{ entries = append(entries, cchat.CompletionEntry{
Raw: msg.Author.Mention(), Raw: msg.Author.Mention(),
Text: message.RenderAuthorName(user), Text: message.RenderUserName(user),
Secondary: text.Plain(msg.Author.Username + "#" + msg.Author.Discriminator), Secondary: text.Plain(msg.Author.Username + "#" + msg.Author.Discriminator),
IconURL: msg.Author.AvatarURL(), IconURL: msg.Author.AvatarURL(),
}) })
@ -252,7 +252,7 @@ func (ch ChannelCompleter) CompleteMentions(word string) []cchat.CompletionEntry
entries = append(entries, cchat.CompletionEntry{ entries = append(entries, cchat.CompletionEntry{
Raw: raw, Raw: raw,
Text: message.RenderAuthorName(user), Text: message.RenderUserName(user),
Secondary: text.Plain(m.User.Username + "#" + m.User.Discriminator), Secondary: text.Plain(m.User.Username + "#" + m.User.Discriminator),
IconURL: user.Avatar(), IconURL: user.Avatar(),
}) })

View File

@ -1,4 +1,4 @@
package send package sender
import ( import (
"github.com/diamondburned/arikawa/v2/api" "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/json/option"
"github.com/diamondburned/arikawa/v2/utils/sendpart" "github.com/diamondburned/arikawa/v2/utils/sendpart"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" "github.com/diamondburned/cchat-discord/internal/config"
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared" "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/sender/completer"
"github.com/diamondburned/cchat-discord/internal/discord/config" "github.com/diamondburned/cchat-discord/internal/discord/session/channel/shared"
"github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/discord/state"
) )
var ( var allowAllMention = []api.AllowedMentionType{
allowAllMention = []api.AllowedMentionType{ api.AllowEveryoneMention,
api.AllowEveryoneMention, api.AllowRoleMention,
api.AllowRoleMention, api.AllowUserMention,
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{ var send = api.SendMessageData{
Content: msg.Content(), Content: msg.Content(),
} }
@ -47,7 +48,8 @@ func WrapMessage(s *state.Instance, msg cchat.SendableMessage) api.SendMessageDa
RepliedUser: option.False, 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 send.AllowedMentions.RepliedUser = option.True
} }
} }
@ -66,7 +68,7 @@ func New(ch shared.Channel) Sender {
} }
func (s Sender) Send(msg cchat.SendableMessage) error { 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 return err
} }
@ -76,7 +78,7 @@ func (s Sender) CanAttach() bool {
} }
func (s Sender) AsCompleter() cchat.Completer { func (s Sender) AsCompleter() cchat.Completer {
return complete.New(s.Channel) return completer.New(s.Channel)
} }
func addAttachments(atts []cchat.MessageAttachment) []sendpart.File { func addAttachments(atts []cchat.MessageAttachment) []sendpart.File {

View File

@ -7,8 +7,8 @@ import (
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/guild" "github.com/diamondburned/cchat-discord/internal/discord/session/guild"
"github.com/diamondburned/cchat-discord/internal/discord/shared/state" "github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/segments/colored" "github.com/diamondburned/cchat-discord/internal/segments/colored"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/cchat/utils/empty"

View File

@ -6,10 +6,10 @@ import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/category" "github.com/diamondburned/cchat-discord/internal/discord/session/channel"
"github.com/diamondburned/cchat-discord/internal/discord/channel" "github.com/diamondburned/cchat-discord/internal/discord/session/channel/category"
"github.com/diamondburned/cchat-discord/internal/discord/shared/funcutil"
"github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/funcutil"
"github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/cchat/utils/empty"
"github.com/pkg/errors" "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) { 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 } 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) { func (g *Guild) Servers(container cchat.ServersContainer) (func(), error) {
c, err := g.state.Channels(g.id) c, err := g.state.Channels(g.id)
if err != nil { 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). // 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) container.SetServers(chs)
// TODO: account for insertion/deletion.
// TODO: RACEEEEEEEEEEEEEEEEEEEEEEE CONDITION!!!!!!!!!!!! // TODO: RACEEEEEEEEEEEEEEEEEEEEEEE CONDITION!!!!!!!!!!!!
// TODO: Add channel stuff. // TODO: Add channel stuff.
stop := funcutil.JoinCancels() stop := funcutil.JoinCancels()
// TODO: account for insertion/deletion.
return stop, nil return stop, nil
} }

View File

@ -7,8 +7,8 @@ import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "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/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"
"github.com/diamondburned/cchat-discord/internal/discord/state/nonce" "github.com/diamondburned/cchat-discord/internal/discord/state/nonce"
"github.com/diamondburned/cchat-discord/internal/funcutil" "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), messages: make(messageList, 0, 100),
} }
hubServer.sender.completers.Prefixes = complete.CompleterPrefixes{ hubServer.sender.completers.Prefixes = completer.CompleterPrefixes{
':': func(word string) []cchat.CompletionEntry { ':': func(word string) []cchat.CompletionEntry {
return complete.Emojis(s, 0, word) return completer.Emojis(s, 0, word)
}, },
'@': func(word string) []cchat.CompletionEntry { '@': func(word string) []cchat.CompletionEntry {
if word != "" { if word != "" {
return complete.AllUsers(s, word) return completer.AllUsers(s, word)
} }
hubServer.msgMutex.Lock() hubServer.msgMutex.Lock()
defer hubServer.msgMutex.Unlock() defer hubServer.msgMutex.Unlock()
return complete.MessageMentions(hubServer.messages) return completer.MessageMentions(hubServer.messages)
}, },
'#': func(word string) []cchat.CompletionEntry { '#': 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 := mention.NewUser(msg.Author)
user.WithState(msgs.state.State) user.WithState(msgs.state.State)
var author = message.NewAuthor(user) var author = message.NewAuthor(msgs.state, user)
if isReply { if isReply {
c, err := msgs.state.Channel(msg.ChannelID) c, err := msgs.state.Channel(msg.ChannelID)
if err == nil { if err == nil {

View File

@ -6,8 +6,8 @@ import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send" "github.com/diamondburned/cchat-discord/internal/discord/session/channel/messenger/sender"
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete" "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"
"github.com/diamondburned/cchat-discord/internal/discord/state/nonce" "github.com/diamondburned/cchat-discord/internal/discord/state/nonce"
"github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/cchat/utils/empty"
@ -28,7 +28,7 @@ type Sender struct {
sentMsgs *nonce.Set sentMsgs *nonce.Set
state *state.Instance state *state.Instance
completers complete.Completer completers completer.Completer
} }
// mentionRegex matche the following: // mentionRegex matche the following:
@ -89,7 +89,7 @@ func (s *Sender) Send(sendable cchat.SendableMessage) error {
s.adder.AddChannel(s.state, channel) 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]) sendData.Content = strings.TrimPrefix(content, matches[0])
// Store the nonce. // Store the nonce.

View File

@ -1,6 +1,7 @@
package hub package hub
import ( import (
"context"
"sync" "sync"
"time" "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) 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 // ActiveChannelIDs returns the list of active channel IDs, that is, the channel
// IDs that should be displayed separately. // IDs that should be displayed separately.

View File

@ -1,14 +1,15 @@
package private package private
import ( import (
"context"
"sort" "sort"
"sync" "sync"
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "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/private/hub" "github.com/diamondburned/cchat-discord/internal/discord/session/private/hub"
"github.com/diamondburned/cchat-discord/internal/discord/state" "github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat/text" "github.com/diamondburned/cchat/text"
"github.com/diamondburned/cchat/utils/empty" "github.com/diamondburned/cchat/utils/empty"
@ -90,8 +91,9 @@ func (priv Private) ID() cchat.ID {
return "!!!private-container!!!" return "!!!private-container!!!"
} }
func (priv Private) Name() text.Rich { func (priv Private) Name(_ context.Context, l cchat.LabelContainer) (func(), error) {
return text.Plain("Private Channels") l.SetLabel(text.Plain("Private Channels"))
return func() {}, nil
} }
func (priv Private) AsLister() cchat.Lister { return priv } func (priv Private) AsLister() cchat.Lister { return priv }
@ -115,7 +117,7 @@ func (active activeChannel) LastMessageID() discord.MessageID {
return discord.MessageID(active.Channel.ID) 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() activeIDs := priv.hub.ActiveChannelIDs()
channels := make([]activeChannel, 0, len(activeIDs)) channels := make([]activeChannel, 0, len(activeIDs))
@ -123,7 +125,7 @@ func (priv Private) Servers(container cchat.ServersContainer) error {
for _, id := range activeIDs { for _, id := range activeIDs {
c, err := priv.state.Channel(id) c, err := priv.state.Channel(id)
if err != nil { 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{ channels = append(channels, activeChannel{
@ -144,7 +146,7 @@ func (priv Private) Servers(container cchat.ServersContainer) error {
for i, ch := range channels { for i, ch := range channels {
c, err := channel.New(priv.state, *ch.Channel) c, err := channel.New(priv.state, *ch.Channel)
if err != nil { 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 servers[i+1] = c
@ -152,5 +154,5 @@ func (priv Private) Servers(container cchat.ServersContainer) error {
container.SetServers(servers) container.SetServers(servers)
priv.containers.Register(container) priv.containers.Register(container)
return nil return func() {}, nil
} }

View File

@ -7,13 +7,11 @@ import (
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/arikawa/v2/session" "github.com/diamondburned/arikawa/v2/session"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/folder" "github.com/diamondburned/cchat-discord/internal/discord/session/guild"
"github.com/diamondburned/cchat-discord/internal/discord/guild" "github.com/diamondburned/cchat-discord/internal/discord/session/guild/folder"
"github.com/diamondburned/cchat-discord/internal/discord/private" "github.com/diamondburned/cchat-discord/internal/discord/session/private"
"github.com/diamondburned/cchat-discord/internal/discord/shared/state" "github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/funcutil" "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/cchat/utils/empty"
"github.com/diamondburned/ningen/v2" "github.com/diamondburned/ningen/v2"
"github.com/pkg/errors" "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) { func (s *Session) Name(ctx context.Context, l cchat.LabelContainer) (func(), error) {
u, err := s.state.Cabinet.Me() return s.state.Labels.AddPresenceLabel(s.state.UserID, l), nil
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
} }
func (s *Session) Disconnect() error { func (s *Session) Disconnect() error {

View File

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

View File

@ -6,16 +6,16 @@ import (
) )
type labelContainers struct { type labelContainers struct {
guilds map[discord.GuildID]guildContainer guilds map[discord.GuildID]guildContainer
channels map[discord.ChannelID]labelerList channels map[discord.ChannelID]labelerList
// presences map[discord.UserID]labelerList presences map[discord.UserID]labelerList
} }
func newLabelContainers() labelContainers { func newLabelContainers() labelContainers {
return labelContainers{ return labelContainers{
guilds: map[discord.GuildID]guildContainer{}, guilds: map[discord.GuildID]guildContainer{},
channels: map[discord.ChannelID]labelerList{}, channels: map[discord.ChannelID]labelerList{},
// presences: map[discord.UserID]labelerList{}, presences: map[discord.UserID]labelerList{},
} }
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/funcutil"
"github.com/diamondburned/cchat-discord/internal/segments/mention" "github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/diamondburned/ningen/v2" "github.com/diamondburned/ningen/v2"
) )
@ -17,7 +18,7 @@ import (
// adder function will do nothing and will return a callback that does nothing. // adder function will do nothing and will return a callback that does nothing.
type Repository struct { type Repository struct {
state *ningen.State state *ningen.State
detachs []func() detach func()
stopped bool stopped bool
mutex sync.Mutex mutex sync.Mutex
@ -31,13 +32,15 @@ func NewRepository(state *ningen.State) *Repository {
stores: newLabelContainers(), stores: newLabelContainers(),
} }
r.detachs = []func(){ r.detach = funcutil.JoinCancels(
state.AddHandler(r.onGuildUpdate), state.AddHandler(r.onGuildUpdate),
state.AddHandler(r.onMemberUpdate), state.AddHandler(r.onMemberUpdate),
state.AddHandler(r.onMemberRemove), state.AddHandler(r.onMemberRemove),
state.AddHandler(r.onChannelUpdate), state.AddHandler(r.onChannelUpdate),
state.AddHandler(r.onChannelDelete), state.AddHandler(r.onChannelDelete),
} state.AddHandler(r.onGuildMembersChunk),
// TODO: *gateway.GuildMemberListUpdate
)
return &r 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. // Repository for more documentation.
func (r *Repository) AddMemberLabel( func (r *Repository) AddMemberLabel(
guildID discord.GuildID, userID discord.UserID, l cchat.LabelContainer) func() { 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)) l.SetLabel(mention.NewMemberText(r.state, guildID, userID))
r.mutex.Lock() 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. // Stop detaches all handlers.
func (r *Repository) Stop() { func (r *Repository) Stop() {
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock()
r.stopped = true r.stopped = true
r.mutex.Unlock()
for _, detach := range r.detachs { r.detach()
detach()
}
} }

View File

@ -5,11 +5,11 @@ import (
"context" "context"
"log" "log"
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/session" "github.com/diamondburned/arikawa/v2/session"
"github.com/diamondburned/arikawa/v2/state" "github.com/diamondburned/arikawa/v2/state"
"github.com/diamondburned/arikawa/v2/state/store/defaultstore" "github.com/diamondburned/arikawa/v2/state/store/defaultstore"
"github.com/diamondburned/arikawa/v2/utils/httputil/httpdriver"
"github.com/diamondburned/cchat" "github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/state/labels" "github.com/diamondburned/cchat-discord/internal/discord/state/labels"
"github.com/diamondburned/cchat-discord/internal/discord/state/nonce" "github.com/diamondburned/cchat-discord/internal/discord/state/nonce"

View File

@ -63,14 +63,20 @@ func NewChannelText(s *ningen.State, chID discord.ChannelID) text.Rich {
} }
rich := text.Rich{Content: ChannelName(*ch)} rich := text.Rich{Content: ChannelName(*ch)}
rich.Segments = []text.Segment{ segment := Segment{
Segment{ Start: 0,
Start: 0, End: len(rich.Content),
End: len(rich.Content),
Channel: NewChannel(*ch),
},
} }
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 return rich
} }

View File

@ -91,6 +91,11 @@ func (um *User) UserID() discord.UserID {
return um.user.ID 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. // SetGuildID sets the user's guild ID.
func (um *User) WithGuildID(guildID discord.GuildID) { func (um *User) WithGuildID(guildID discord.GuildID) {
um.guildID = guildID um.guildID = guildID