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
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 {

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"
)
var World = &registry{
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
}

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

View File

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

View File

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

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"
"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,
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 {
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{},
}
}

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/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()
}

View File

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

View File

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

View File

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