almost there...

This commit is contained in:
diamondburned 2021-03-13 03:49:23 -08:00
parent 3e9bed8273
commit 7bfe466482
54 changed files with 495 additions and 115 deletions

View File

@ -0,0 +1,83 @@
package folder
import (
"context"
"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/shared/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(ctx context.Context, l cchat.LabelContainer) (func(), error) {
var name = text.Rich{
Content: gf.GuildFolder.Name,
}
if gf.GuildFolder.Color > 0 {
name.Segments = []text.Segment{
colored.New(len(name.Content), gf.GuildFolder.Color.Uint32()),
}
}
// TODO: add folder updater from setting update events.
return func() {}, nil
}
// IsLister returns true.
func (gf *GuildFolder) AsLister() cchat.Lister { return gf }
func (gf *GuildFolder) Servers(container cchat.ServersContainer) (func(), 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 an empty callback. We're lazily redoing the whole list when a
// guild moves for now.
return func() {}, nil
}

View File

@ -0,0 +1,101 @@
package guild
import (
"context"
"sort"
"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/state"
"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(ctx context.Context, l cchat.LabelContainer) (func(), error) {
return g.state.Labels.AddGuildLabel(g.id, l)
}
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")
}
// 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 nil, errors.Wrapf(err, "Failed to make channel %q: %v", ch.Name, err)
}
chs = append(chs, c)
default:
continue
}
}
container.SetServers(chs)
// TODO: RACEEEEEEEEEEEEEEEEEEEEEEE CONDITION!!!!!!!!!!!!
// TODO: Add channel stuff.
stop := funcutil.JoinCancels()
// TODO: account for insertion/deletion.
return stop, nil
}

View File

@ -0,0 +1,175 @@
package session
import (
"context"
"time"
"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/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"
)
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(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
}
func (s *Session) Disconnect() error {
return s.state.CloseGracefully()
}
func (s *Session) AsSessionSaver() cchat.SessionSaver { return s.state }
func (s *Session) Servers(container cchat.ServersContainer) (func(), error) {
if err := s.servers(container); err != nil {
return nil, err
}
retryFn := func() {
// We should set up a back-off here.
for s.servers(container) != nil {
time.Sleep(5 * time.Second)
}
}
stop := funcutil.JoinCancels(
// 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) {
retryFn()
}),
// Update the entire container when we update the guild list. Blame
// Discord on this one.
s.state.AddHandler(func(update *gateway.UserSettingsUpdateEvent) {
if update.GuildFolders != nil || update.GuildPositions != nil {
retryFn()
}
}),
)
return stop, nil
}
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.
// TODO: correct how? What did I mean by this?
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

@ -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/segments/mention"
"github.com/diamondburned/ningen/v2"
)
@ -54,7 +55,7 @@ func (r *Repository) onGuildUpdate(ev *gateway.GuildUpdateEvent) {
return
}
rich := labelGuild(r.state, ev.ID)
rich := mention.NewGuildText(r.state, ev.ID)
for labeler := range guild.guild {
labeler.SetLabel(rich)
@ -64,7 +65,7 @@ func (r *Repository) onGuildUpdate(ev *gateway.GuildUpdateEvent) {
// AddGuildLabel adds a label to display the given guild ID. Refer to Repository
// for more documentation.
func (r *Repository) AddGuildLabel(guildID discord.GuildID, l cchat.LabelContainer) func() {
l.SetLabel(labelGuild(r.state, guildID))
l.SetLabel(mention.NewGuildText(r.state, guildID))
r.mutex.Lock()
defer r.mutex.Unlock()
@ -89,16 +90,37 @@ func (r *Repository) AddGuildLabel(guildID discord.GuildID, l cchat.LabelContain
}
}
func (r *Repository) onMemberRemove(ev *gateway.GuildMemberRemoveEvent) {}
func (r *Repository) onMemberRemove(ev *gateway.GuildMemberRemoveEvent) {
// Not sure what to do.
}
func (r *Repository) onMemberUpdate(ev *gateway.GuildMemberUpdateEvent) {}
func (r *Repository) onMemberUpdate(ev *gateway.GuildMemberUpdateEvent) {
r.mutex.Lock()
defer r.mutex.Unlock()
if r.stopped {
return
}
guild, _ := r.stores.guilds[ev.GuildID]
member, ok := guild.members[ev.User.ID]
if !ok {
return
}
rich := mention.NewMemberText(r.state, ev.GuildID, ev.User.ID)
for labeler := range member {
labeler.SetLabel(rich)
}
}
// AddMemberLabel adds a label to display the given member live. Refer to
// Repository for more documentation.
func (r *Repository) AddMemberLabel(
guildID discord.GuildID, userID discord.UserID, l cchat.LabelContainer) func() {
l.SetLabel(labelMember(r.state, guildID, userID))
l.SetLabel(mention.NewMemberText(r.state, guildID, userID))
r.mutex.Lock()
defer r.mutex.Unlock()
@ -134,13 +156,34 @@ func (r *Repository) AddMemberLabel(
}
}
func (r *Repository) onChannelUpdate(ev *gateway.ChannelUpdateEvent) {}
func (r *Repository) onChannelDelete(ev *gateway.ChannelDeleteEvent) {}
func (r *Repository) onChannelDelete(ev *gateway.ChannelDeleteEvent) {
// Not sure what to do.
}
func (r *Repository) onChannelUpdate(ev *gateway.ChannelUpdateEvent) {
r.mutex.Lock()
defer r.mutex.Unlock()
if r.stopped {
return
}
channel, ok := r.stores.channels[ev.ID]
if !ok {
return
}
rich := mention.NewChannelText(r.state, ev.ID)
for labeler := range channel {
labeler.SetLabel(rich)
}
}
// AddChannelLabel adds a label to display the given channel live. Refer to
// Repository for more documentation.
func (r *Repository) AddChannelLabel(chID discord.ChannelID, l cchat.LabelContainer) func() {
l.SetLabel(labelChannel(r.state, chID))
l.SetLabel(mention.NewChannelText(r.state, chID))
r.mutex.Lock()
defer r.mutex.Unlock()

View File

@ -3,7 +3,9 @@ package state
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"
@ -66,12 +68,12 @@ func New(s *state.State) (*Instance, error) {
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
// },
// )
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

View File

@ -1,73 +0,0 @@
package labels
import (
"github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat-discord/internal/segments/avatar"
"github.com/diamondburned/cchat-discord/internal/segments/colored"
"github.com/diamondburned/cchat-discord/internal/segments/mention"
"github.com/diamondburned/cchat-discord/internal/segments/segutil"
"github.com/diamondburned/cchat-discord/internal/urlutils"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/ningen/v2"
)
// TODO: these functions should probably be its own package.
func labelGuild(s *ningen.State, guildID discord.GuildID) text.Rich {
g, err := s.Cabinet.Guild(guildID)
if err != nil {
return text.Plain(guildID.String())
}
return text.Rich{
Content: g.Name,
Segments: []text.Segment{
avatar.Segment{
URL: urlutils.AvatarURL(g.IconURL()),
Size: urlutils.AvatarSize,
Text: g.Name,
},
},
}
}
func labelMember(s *ningen.State, g discord.GuildID, u discord.UserID) text.Rich {
m, err := s.Cabinet.Member(g, u)
if err != nil {
s.MemberState.RequestMember(g, u)
return text.Plain(u.Mention())
}
user := mention.NewUser(m.User)
user.WithMember(*m)
user.WithGuildID(g)
user.WithState(s)
user.Prefetch()
rich := text.Rich{Content: user.DisplayName()}
rich.Segments = []text.Segment{
mention.NewSegment(0, len(rich.Content), user),
}
if m.User.Bot {
rich.Content += " "
rich.Segments = append(rich.Segments,
colored.NewBlurple(segutil.Write(&rich, "[BOT]")),
)
}
return rich
}
func labelChannel(s *ningen.State, chID discord.ChannelID) text.Rich {
var rich text.Rich
ch, err := s.Cabinet.Channel(chID)
if err != nil {
rich = text.Plain(ch.Mention())
} else {
rich = text.Plain(mention.ChannelName(*ch))
}
return rich
}

View File

@ -54,6 +54,26 @@ func FormatRecipients(users []discord.User) string {
return strings.Join(usernames, ", ") + " and " + users[len(users)-1].Username
}
// NewChannelText creates a new rich text describing the given channel fetched
// from the state.
func NewChannelText(s *ningen.State, chID discord.ChannelID) text.Rich {
ch, err := s.Cabinet.Channel(chID)
if err != nil {
return text.Plain(ch.Mention())
}
rich := text.Rich{Content: ChannelName(*ch)}
rich.Segments = []text.Segment{
Segment{
Start: 0,
End: len(rich.Content),
Channel: NewChannel(*ch),
},
}
return rich
}
type Channel struct {
discord.Channel
}

View File

@ -0,0 +1,29 @@
package mention
import (
"github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/cchat-discord/internal/segments/avatar"
"github.com/diamondburned/cchat-discord/internal/urlutils"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/ningen/v2"
)
// NewGuildText creates a new rich text describing the given member fetched from
// the state.
func NewGuildText(s *ningen.State, guildID discord.GuildID) text.Rich {
g, err := s.Cabinet.Guild(guildID)
if err != nil {
return text.Plain(guildID.String())
}
return text.Rich{
Content: g.Name,
Segments: []text.Segment{
avatar.Segment{
URL: urlutils.AvatarURL(g.IconURL()),
Size: urlutils.AvatarSize,
Text: g.Name,
},
},
}
}

View File

@ -12,41 +12,41 @@ import (
"github.com/diamondburned/cchat-discord/internal/segments/segutil"
"github.com/diamondburned/cchat-discord/internal/urlutils"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/cchat/utils/empty"
"github.com/diamondburned/ningen/v2"
)
// NameSegment represents a clickable member name.
type NameSegment struct {
empty.TextSegment
start int
end int
um User
}
var _ text.Segment = (*NameSegment)(nil)
func NewSegment(start, end int, user *User) NameSegment {
return NameSegment{
start: start,
end: end,
um: *user,
// NewMemberText creates a new rich text describing the given member fetched
// from the state.
func NewMemberText(s *ningen.State, g discord.GuildID, u discord.UserID) text.Rich {
m, err := s.Cabinet.Member(g, u)
if err != nil {
s.MemberState.RequestMember(g, u)
return text.Plain(u.Mention())
}
}
func (m NameSegment) Bounds() (start, end int) {
return m.start, m.end
}
user := NewUser(m.User)
user.WithMember(*m)
user.WithGuildID(g)
user.WithState(s)
user.Prefetch()
func (m NameSegment) AsMentioner() text.Mentioner { return &m.um }
func (m NameSegment) AsAvatarer() text.Avatarer { return &m.um }
// AsColorer only returns User if the user actually has a colored role.
func (m NameSegment) AsColorer() text.Colorer {
if m.um.HasColor() {
return &m.um
rich := text.Rich{Content: user.DisplayName()}
rich.Segments = []text.Segment{
Segment{
Start: 0,
End: len(rich.Content),
User: user,
},
}
return nil
if m.User.Bot {
rich.Content += " "
rich.Segments = append(rich.Segments,
colored.NewBlurple(segutil.Write(&rich, "[BOT]")),
)
}
return rich
}
type User struct {