Added WIP private messages, removed nonce

This commit is contained in:
diamondburned 2020-12-17 00:01:58 -08:00
parent fc795ef7b6
commit 1155ccac34
29 changed files with 649 additions and 93 deletions

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.14
require (
github.com/diamondburned/arikawa v1.3.6
github.com/diamondburned/cchat v0.3.11
github.com/diamondburned/cchat v0.3.12
github.com/diamondburned/ningen v0.2.1-0.20201023061015-ce64ffb0bb12
github.com/dustin/go-humanize v1.0.0
github.com/go-test/deep v1.0.7

2
go.sum
View File

@ -103,6 +103,8 @@ github.com/diamondburned/cchat v0.3.8 h1:vgFe8giVfwsAO+WpTYsTDIXvRUN48osVPNu0pZN
github.com/diamondburned/cchat v0.3.8/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg=
github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.12 h1:mew54lsDrwrJs4U2FtdbNFl/wAZcueIgZCsImHQzVL4=
github.com/diamondburned/cchat v0.3.12/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I=
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
github.com/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M=

View File

@ -13,21 +13,28 @@ import (
type Channel struct {
empty.Server
*shared.Channel
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.
_, err := s.Permissions(ch.ID, s.UserID)
if err != nil {
return nil, errors.Wrap(err, "Failed to get permission")
return Channel{}, errors.Wrap(err, "Failed to get permission")
}
sharedCh := &shared.Channel{
sharedCh := shared.Channel{
ID: ch.ID,
GuildID: ch.GuildID,
State: s,
@ -50,7 +57,7 @@ func (ch Channel) Name() text.Rich {
}
if c.NSFW {
return text.Rich{Content: "#!" + c.Name}
return text.Rich{Content: "#" + c.Name + " (nsfw)"}
} else {
return text.Rich{Content: "#" + c.Name}
}

View File

@ -11,11 +11,11 @@ import (
)
type Commander struct {
*shared.Channel
shared.Channel
msgCompl complete.Completer
}
func NewCommander(ch *shared.Channel) cchat.Commander {
func NewCommander(ch shared.Channel) cchat.Commander {
return Commander{
Channel: ch,
msgCompl: complete.Completer{

View File

@ -10,7 +10,7 @@ type Command struct {
Name string
Args Arguments
Desc string
RunFunc func(*shared.Channel, []string) ([]byte, error) // words[1:]
RunFunc func(shared.Channel, []string) ([]byte, error) // words[1:]
}
func (cmd Command) writeHelp(builder *bytes.Buffer) {

View File

@ -29,7 +29,7 @@ func (cmds Commands) Help() []byte {
// Run runs a command with the given words. It errors out if the command is not
// found.
func (cmds Commands) Run(ch *shared.Channel, words []string) ([]byte, error) {
func (cmds Commands) Run(ch shared.Channel, words []string) ([]byte, error) {
if words[0] == "help" {
return cmds.Help(), nil
}
@ -80,7 +80,7 @@ var World = Commands{
Name: "send-embed",
Args: Arguments{"-t title", "-c color", "description"},
Desc: "Send a basic embed to the current channel",
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
var embed discord.Embed
var color uint // no Uint32Var
@ -107,7 +107,7 @@ var World = Commands{
{
Name: "info",
Desc: "Print information as JSON",
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
channel, err := ch.State.Channel(ch.ID)
if err != nil {
return nil, errors.Wrap(err, "failed to get channel")
@ -124,7 +124,7 @@ var World = Commands{
{
Name: "list-channels",
Desc: "Print all channels of this guild and their topics",
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
channels, err := ch.State.Channels(ch.GuildID)
if err != nil {
return nil, errors.Wrap(err, "failed to get channels")
@ -142,7 +142,7 @@ var World = Commands{
Name: "presence",
Args: Arguments{"mention:user"},
Desc: "Print JSON of a member/user's presence state",
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
if err := assertArgc(argv, 1); err != nil {
return nil, err
}
@ -164,7 +164,7 @@ var World = Commands{
Name: "member",
Args: Arguments{"mention:user"},
Desc: "Print JSON of a member/user's member state",
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
if err := assertArgc(argv, 1); err != nil {
return nil, err
}

View File

@ -8,12 +8,12 @@ import (
)
type Actioner struct {
*shared.Channel
shared.Channel
}
var _ cchat.Actioner = (*Actioner)(nil)
func New(ch *shared.Channel) Actioner {
func New(ch shared.Channel) Actioner {
return Actioner{ch}
}

View File

@ -11,10 +11,10 @@ import (
)
type Backlogger struct {
*shared.Channel
shared.Channel
}
func New(ch *shared.Channel) cchat.Backlogger {
func New(ch shared.Channel) cchat.Backlogger {
return Backlogger{ch}
}

View File

@ -8,10 +8,10 @@ import (
)
type Editor struct {
*shared.Channel
shared.Channel
}
func New(ch *shared.Channel) cchat.Editor {
func New(ch shared.Channel) cchat.Editor {
return Editor{ch}
}

View File

@ -10,10 +10,10 @@ import (
)
type TypingIndicator struct {
*shared.Channel
shared.Channel
}
func NewTyping(ch *shared.Channel) cchat.TypingIndicator {
func NewTyping(ch shared.Channel) cchat.TypingIndicator {
return TypingIndicator{ch}
}

View File

@ -8,10 +8,10 @@ import (
)
type UnreadIndicator struct {
*shared.Channel
shared.Channel
}
func NewUnread(ch *shared.Channel) cchat.UnreadIndicator {
func NewUnread(ch shared.Channel) cchat.UnreadIndicator {
return UnreadIndicator{ch}
}

View File

@ -17,13 +17,13 @@ import (
)
type Member struct {
channel *shared.Channel
channel shared.Channel
userID discord.UserID
origName string // use if cache is stale
}
// New creates a new list member. it.Member must not be nil.
func NewMember(ch *shared.Channel, opItem gateway.GuildMemberListOpItem) cchat.ListMember {
func NewMember(ch shared.Channel, opItem gateway.GuildMemberListOpItem) cchat.ListMember {
return &Member{
channel: ch,
userID: opItem.Member.User.ID,

View File

@ -31,10 +31,10 @@ func seekPrevGroup(l *member.List, ix int) (item, group gateway.GuildMemberListO
}
type MemberLister struct {
*shared.Channel
shared.Channel
}
func New(ch *shared.Channel) cchat.MemberLister {
func New(ch shared.Channel) cchat.MemberLister {
return MemberLister{ch}
}

View File

@ -23,7 +23,7 @@ type Section struct {
}
func NewSection(
ch *shared.Channel,
ch shared.Channel,
listID string,
group gateway.GuildMemberListGroup) cchat.MemberSection {
@ -78,7 +78,7 @@ func (s Section) AsMemberDynamicSection() cchat.MemberDynamicSection {
func (s Section) IsMemberDynamicSection() bool { return true }
type DynamicSection struct {
*shared.Channel
shared.Channel
}
var _ cchat.MemberDynamicSection = (*DynamicSection)(nil)

View File

@ -23,12 +23,12 @@ import (
type Messenger struct {
empty.Messenger
*shared.Channel
shared.Channel
}
var _ cchat.Messenger = (*Messenger)(nil)
func New(ch *shared.Channel) Messenger {
func New(ch shared.Channel) Messenger {
return Messenger{Channel: ch}
}
@ -129,7 +129,7 @@ func (msgr Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContainer
}),
)
return funcutil.JoinCancels(addcancel()), nil
return funcutil.JoinCancels(addcancel()...), nil
}
func (msgr Messenger) AsSender() cchat.Sender {

View File

@ -12,10 +12,10 @@ import (
)
type Nicknamer struct {
*shared.Channel
shared.Channel
}
func New(ch *shared.Channel) cchat.Nicknamer {
func New(ch shared.Channel) cchat.Nicknamer {
return Nicknamer{ch}
}

View File

@ -8,12 +8,12 @@ import (
)
type Completer struct {
*shared.Channel
shared.Channel
}
const MaxCompletion = 15
func New(ch *shared.Channel) cchat.Completer {
func New(ch shared.Channel) cchat.Completer {
return Completer{ch}
}

View File

@ -3,40 +3,50 @@ package complete
import (
"strings"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/urlutils"
"github.com/diamondburned/cchat/text"
)
func (ch Completer) CompleteEmojis(word string) (entries []cchat.CompletionEntry) {
return CompleteEmojis(ch.State, ch.GuildID, word)
}
func CompleteEmojis(s *state.Instance, gID discord.GuildID, word string) []cchat.CompletionEntry {
// Ignore if empty word.
if word == "" {
return
return nil
}
e, err := ch.State.EmojiState.Get(ch.GuildID)
e, err := s.EmojiState.Get(gID)
if err != nil {
return
return nil
}
var match = strings.ToLower(word)
var entries = make([]cchat.CompletionEntry, 0, MaxCompletion)
for _, guild := range e {
for _, emoji := range guild.Emojis {
if contains(match, emoji.Name) {
entries = append(entries, cchat.CompletionEntry{
Raw: emoji.String(),
Text: text.Rich{Content: ":" + emoji.Name + ":"},
Secondary: text.Rich{Content: guild.Name},
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
Image: true,
})
if len(entries) >= MaxCompletion {
return
}
if !contains(match, emoji.Name) {
continue
}
entries = append(entries, cchat.CompletionEntry{
Raw: emoji.String(),
Text: text.Rich{Content: ":" + emoji.Name + ":"},
Secondary: text.Rich{Content: guild.Name},
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
Image: true,
})
if len(entries) >= MaxCompletion {
return entries
}
}
}
return
return entries
}

View File

@ -6,28 +6,30 @@ import (
"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/state"
)
type Sender struct {
*shared.Channel
shared.Channel
}
var _ cchat.Sender = (*Sender)(nil)
func New(ch *shared.Channel) Sender {
func New(ch shared.Channel) Sender {
return Sender{ch}
}
func (s Sender) Send(msg cchat.SendableMessage) error {
return Send(s.State, s.ID, msg)
}
func Send(s *state.Instance, chID discord.ChannelID, msg cchat.SendableMessage) error {
var send = api.SendMessageData{Content: msg.Content()}
if noncer := msg.AsNoncer(); noncer != nil {
send.Nonce = noncer.Nonce()
}
if attacher := msg.AsAttachments(); attacher != nil {
send.Files = addAttachments(attacher.Attachments())
}
_, err := s.State.SendMessageComplex(s.ID, send)
_, err := s.SendMessageComplex(chID, send)
return err
}

View File

@ -0,0 +1,64 @@
package channel
import (
"context"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/urlutils"
"github.com/pkg/errors"
)
type Private struct {
Channel
}
var _ cchat.Server = (*Private)(nil)
func NewPrivate(s *state.Instance, ch discord.Channel) (cchat.Server, error) {
if ch.GuildID.IsValid() {
return nil, errors.New("channel has valid guild ID: not a DM")
}
channel, err := NewChannel(s, ch)
if err != nil {
return nil, err
}
return Private{Channel: channel}, nil
}
func (priv Private) AsIconer() cchat.Iconer {
return NewAvatarIcon(priv.State)
}
type AvatarIcon struct {
State *state.Instance
}
func NewAvatarIcon(state *state.Instance) cchat.Iconer {
return AvatarIcon{state}
}
func (avy AvatarIcon) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
u, err := avy.State.WithContext(ctx).Me()
if err != nil {
// This shouldn't happen.
return nil, errors.Wrap(err, "Failed to get guild")
}
// Used for comparison.
if u.Avatar != "" {
iconer.SetIcon(urlutils.AvatarURL(u.AvatarURL()))
}
selfID := u.ID
return avy.State.AddHandler(func(update *gateway.UserUpdateEvent) {
if selfID == update.ID {
iconer.SetIcon(urlutils.AvatarURL(update.AvatarURL()))
}
}), nil
}

View File

@ -1,6 +1,8 @@
package shared
import (
"errors"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/cchat-discord/internal/discord/state"
)
@ -33,6 +35,9 @@ func (ch Channel) Messages() ([]discord.Message, error) {
}
func (ch Channel) Guild() (*discord.Guild, error) {
if !ch.GuildID.IsValid() {
return nil, errors.New("channel not in guild")
}
return ch.State.Store.Guild(ch.GuildID)
}

View File

@ -17,7 +17,6 @@ type messageHeader struct {
time discord.Timestamp
channelID discord.ChannelID
guildID discord.GuildID
nonce string
}
var _ cchat.MessageHeader = (*messageHeader)(nil)
@ -28,7 +27,6 @@ func newHeader(msg discord.Message) messageHeader {
time: msg.Timestamp,
channelID: msg.ChannelID,
guildID: msg.GuildID,
nonce: msg.Nonce,
}
if msg.EditedTimestamp.IsValid() {
h.time = msg.EditedTimestamp
@ -49,6 +47,10 @@ func (m messageHeader) ID() cchat.ID {
return m.id.String()
}
func (m messageHeader) MessageID() discord.MessageID { return m.id }
func (m messageHeader) ChannelID() discord.ChannelID { return m.channelID }
func (m messageHeader) GuildID() discord.GuildID { return m.guildID }
func (m messageHeader) Time() time.Time {
return m.time.Time()
}
@ -67,7 +69,6 @@ var (
_ cchat.MessageCreate = (*Message)(nil)
_ cchat.MessageUpdate = (*Message)(nil)
_ cchat.MessageDelete = (*Message)(nil)
_ cchat.Noncer = (*Message)(nil)
)
func NewMessageUpdateContent(msg discord.Message, s *state.Instance) Message {
@ -183,10 +184,6 @@ func (m Message) Content() text.Rich {
return m.content
}
func (m Message) Nonce() string {
return m.nonce
}
func (m Message) Mentioned() bool {
return m.mentioned
}

View File

@ -0,0 +1,149 @@
package hub
import (
"context"
"sync"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/message"
"github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat-discord/internal/funcutil"
"github.com/diamondburned/cchat/utils/empty"
)
const maxMessages = 100
type messageList []discord.Message
func (list messageList) idx(id discord.MessageID) int {
for i, msg := range list {
if msg.ID == id {
return i
}
}
return -1
}
func (list *messageList) append(msg discord.Message) {
*list = append(*list, msg)
// cap the length
if len(*list) > maxMessages {
copy(*list, (*list)[1:]) // shift left once
(*list)[len(*list)-1] = discord.Message{} // nil out last to not memory leak
*list = (*list)[:len(*list)-1] // slice it away
}
}
func (list *messageList) swap(newMsg discord.Message) {
if idx := list.idx(newMsg.ID); idx > -1 {
(*list)[idx] = newMsg
}
}
func (list *messageList) delete(id discord.MessageID) {
if idx := list.idx(id); idx > -1 {
*list = append((*list)[:idx], (*list)[idx+1:]...)
}
}
type Messages struct {
empty.Messenger
state *state.Instance
acList *activeList
sender *Sender
msgMutex sync.Mutex
messages messageList
cancel func()
}
func NewMessages(s *state.Instance, acList *activeList, adder ChannelAdder) *Messages {
hubServer := &Messages{
state: s,
acList: acList,
sender: NewSender(s, acList, adder),
messages: make(messageList, 0, 100),
}
hubServer.cancel = funcutil.JoinCancels(
s.AddHandler(func(msg *gateway.MessageCreateEvent) {
if msg.GuildID.IsValid() || acList.isActive(msg.ChannelID) {
return
}
hubServer.msgMutex.Lock()
hubServer.messages.append(msg.Message)
hubServer.msgMutex.Unlock()
}),
s.AddHandler(func(update *gateway.MessageUpdateEvent) {
if update.GuildID.IsValid() || acList.isActive(update.ChannelID) {
return
}
// The event itself is unreliable, so we must rely on the state.
m, err := hubServer.state.Message(update.ChannelID, update.ID)
if err != nil {
return
}
hubServer.msgMutex.Lock()
hubServer.messages.swap(*m)
hubServer.msgMutex.Unlock()
}),
s.AddHandler(func(del *gateway.MessageDeleteEvent) {
if del.GuildID.IsValid() || acList.isActive(del.ChannelID) {
return
}
hubServer.msgMutex.Lock()
hubServer.messages.delete(del.ID)
hubServer.msgMutex.Unlock()
}),
)
return hubServer
}
func (msgs *Messages) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
msgs.msgMutex.Lock()
for _, msg := range msgs.messages {
ct.CreateMessage(message.NewDirectMessage(msg, msgs.state))
}
msgs.msgMutex.Unlock()
// Bind the handler.
return funcutil.JoinCancels(
msgs.state.AddHandler(func(msg *gateway.MessageCreateEvent) {
if msg.GuildID.IsValid() || msgs.acList.isActive(msg.ChannelID) {
return
}
ct.CreateMessage(message.NewMessageCreate(msg, msgs.state))
msgs.state.ReadState.MarkRead(msg.ChannelID, msg.ID)
}),
msgs.state.AddHandler(func(update *gateway.MessageUpdateEvent) {
if update.GuildID.IsValid() || msgs.acList.isActive(update.ChannelID) {
return
}
ct.UpdateMessage(message.NewMessageUpdateContent(update.Message, msgs.state))
}),
msgs.state.AddHandler(func(del *gateway.MessageDeleteEvent) {
if del.GuildID.IsValid() || msgs.acList.isActive(del.ChannelID) {
return
}
ct.DeleteMessage(message.NewHeaderDelete(del))
}),
), nil
}
func (msgs *Messages) AsSender() cchat.Sender { return msgs.sender }

View File

@ -0,0 +1,75 @@
package hub
import (
"regexp"
"strings"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send"
"github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat/utils/empty"
"github.com/pkg/errors"
)
// ChannelAdder is used to add a new direct message channel into a container.
type ChannelAdder interface {
AddChannel(state *state.Instance, ch *discord.Channel)
}
type Sender struct {
empty.Sender
adder ChannelAdder
acList *activeList
state *state.Instance
}
func NewSender(s *state.Instance, acList *activeList, adder ChannelAdder) *Sender {
return &Sender{adder: adder, acList: acList, state: s}
}
var mentionRegex = regexp.MustCompile(`^<@!?(\d+)> ?`)
// wrappedMessage wraps around a SendableMessage to override its content.
type wrappedMessage struct {
cchat.SendableMessage
content string
}
func (wrMsg wrappedMessage) Content() string {
return wrMsg.content
}
func (s *Sender) CanAttach() bool { return true }
func (s *Sender) Send(sendable cchat.SendableMessage) error {
content := sendable.Content()
// Validate message.
matches := mentionRegex.FindStringSubmatch(content)
if matches == nil {
return errors.New("messages sent here must start with a mention")
}
targetID, err := discord.ParseSnowflake(matches[1])
if err != nil {
return errors.Wrap(err, "failed to parse recipient ID")
}
ch, err := s.state.CreatePrivateChannel(discord.UserID(targetID))
if err != nil {
return errors.Wrap(err, "failed to find DM channel")
}
s.adder.AddChannel(s.state, ch)
s.acList.add(ch.ID)
return send.Send(s.state, ch.ID, wrappedMessage{
SendableMessage: sendable,
content: strings.TrimPrefix(content, matches[0]),
})
}
// func (msgs *Messages) AsCompleter() cchat.Completer {
// return complete.New(msgs)
// }

View File

@ -0,0 +1,103 @@
package hub
import (
"sync"
"time"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/internal/discord/state"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/cchat/utils/empty"
"github.com/pkg/errors"
)
// automatically add all channels with active messages within the past 48 hours.
const autoAddActive = 24 * time.Hour
// activeList contains a list of channel IDs that should be put into its own
// channels.
type activeList struct {
mut sync.Mutex
active map[discord.ChannelID]struct{}
}
func makeActiveList(s *state.Instance) (*activeList, error) {
channels, err := s.PrivateChannels()
if err != nil {
return nil, errors.Wrap(err, "failed to get private channels")
}
ids := make(map[discord.ChannelID]struct{}, len(channels))
now := time.Now()
for _, channel := range channels {
if channel.LastMessageID.Time().Add(autoAddActive).After(now) {
ids[channel.ID] = struct{}{}
}
}
return &activeList{active: ids}, nil
}
func (acList *activeList) list() []discord.ChannelID {
acList.mut.Lock()
defer acList.mut.Unlock()
var channelIDs = make([]discord.ChannelID, 0, len(acList.active))
for channelID := range acList.active {
channelIDs = append(channelIDs, channelID)
}
return channelIDs
}
func (acList *activeList) isActive(channelID discord.ChannelID) bool {
acList.mut.Lock()
defer acList.mut.Unlock()
_, ok := acList.active[channelID]
return ok
}
func (acList *activeList) add(chID discord.ChannelID) {
acList.mut.Lock()
defer acList.mut.Unlock()
acList.active[chID] = struct{}{}
}
// Server is the server (channel) that contains all incoming DM messages that
// are not being listened.
type Server struct {
empty.Server
acList *activeList
msgs *Messages
}
func New(s *state.Instance, adder ChannelAdder) (*Server, error) {
acList, err := makeActiveList(s)
if err != nil {
return nil, errors.Wrap(err, "failed to make active guild list")
}
return &Server{
acList: acList,
msgs: NewMessages(s, acList, adder),
}, nil
}
func (hub *Server) ID() cchat.ID { return "!!!hub-server!!!" }
func (hub *Server) Name() text.Rich { return text.Plain("Incoming Messages") }
// ActiveChannelIDs returns the list of active channel IDs, that is, the channel
// IDs that should be displayed separately.
func (hub *Server) ActiveChannelIDs() []discord.ChannelID {
return hub.acList.list()
}
// Close unbinds the message handlers from the hub, invalidating it forever.
func (hub *Server) Close() { hub.msgs.cancel() }
func (hub *Server) AsMessenger() cchat.Messenger { return hub.msgs }

View File

@ -0,0 +1,127 @@
package private
import (
"sort"
"sync"
"github.com/diamondburned/arikawa/discord"
"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/state"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/cchat/utils/empty"
"github.com/pkg/errors"
)
// I don't think the cchat specs said anything about sharing a cchat.Server, so
// we might need to do this. Nevertheless, it seems overkill.
type containerSet struct {
mut sync.Mutex
set map[cchat.ServersContainer]struct{}
}
func newContainerSet() *containerSet {
return &containerSet{
set: map[cchat.ServersContainer]struct{}{},
}
}
func (cset *containerSet) Register(container cchat.ServersContainer) {
cset.mut.Lock()
cset.set[container] = struct{}{}
cset.mut.Unlock()
}
// prependServer wraps around Server to always prepend this wrapped server on
// top of the servers container.
type prependServer struct{ cchat.Server }
// PreviousID returns the appropriate parameters to prepend this server.
func (ps prependServer) PreviousID() (cchat.ID, bool) { return "", false }
func (cset *containerSet) AddChannel(s *state.Instance, ch *discord.Channel) {
c, err := channel.New(s, *ch)
if err != nil {
return
}
replace := prependServer{Server: c}
cset.mut.Lock()
for container := range cset.set {
container.UpdateServer(replace)
}
cset.mut.Unlock()
}
type Private struct {
empty.Server
state *state.Instance
hub *hub.Server
containers *containerSet
}
func New(s *state.Instance) (cchat.Server, error) {
containers := newContainerSet()
hubServer, err := hub.New(s, containers)
if err != nil {
return nil, errors.Wrap(err, "failed to make hub server")
}
return Private{
state: s,
hub: hubServer,
containers: containers,
}, nil
}
func (priv Private) ID() cchat.ID {
// Not even a number, so no chance of colliding with snowflakes.
return "!!!private-container!!!"
}
func (priv Private) Name() text.Rich {
return text.Plain("Private Channels")
}
func (priv Private) AsLister() cchat.Lister { return priv }
func (priv Private) Servers(container cchat.ServersContainer) error {
activeIDs := priv.hub.ActiveChannelIDs()
channels := make([]*discord.Channel, 0, len(activeIDs))
for _, id := range activeIDs {
c, err := priv.state.Channel(id)
if err != nil {
return errors.Wrap(err, "failed to get private channel")
}
channels = append(channels, c)
}
// Sort so that channels with the largest last message ID (and therefore the
// latest message) will be on top.
sort.Slice(channels, func(i, j int) bool {
return channels[i].LastMessageID > channels[j].LastMessageID
})
servers := make([]cchat.Server, len(channels)+1)
servers[0] = priv.hub
for i, ch := range channels {
c, err := channel.New(priv.state, *ch)
if err != nil {
return errors.Wrap(err, "failed to create server for private channel")
}
servers[i] = c
}
container.SetServers(servers)
priv.containers.Register(container)
return nil
}

View File

@ -8,6 +8,7 @@ import (
"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"
@ -20,22 +21,31 @@ var ErrMFA = session.ErrMFA
type Session struct {
empty.Session
*state.Instance
private cchat.Server
state *state.Instance
}
func NewFromInstance(i *state.Instance) (cchat.Session, error) {
return &Session{Instance: i}, nil
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.UserID.String()
return s.state.UserID.String()
}
func (s *Session) Name() text.Rich {
u, err := s.Store.Me()
u, err := s.state.Store.Me()
if err != nil {
// This shouldn't happen, ever.
return text.Rich{Content: "<@" + s.UserID.String() + ">"}
return text.Rich{Content: "<@" + s.state.UserID.String() + ">"}
}
return text.Rich{Content: u.Username + "#" + u.Discriminator}
@ -44,7 +54,7 @@ func (s *Session) Name() text.Rich {
func (s *Session) AsIconer() cchat.Iconer { return s }
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
u, err := s.Me()
u, err := s.state.Me()
if err != nil {
return nil, errors.Wrap(err, "Failed to get the current user")
}
@ -52,28 +62,28 @@ func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(),
// Thanks to arikawa, AvatarURL is never empty.
iconer.SetIcon(urlutils.AvatarURL(u.AvatarURL()))
return s.AddHandler(func(*gateway.UserUpdateEvent) {
return s.state.AddHandler(func(*gateway.UserUpdateEvent) {
// Bypass the event and use the state cache.
if u, err := s.Store.Me(); err == nil {
if u, err := s.state.Store.Me(); err == nil {
iconer.SetIcon(urlutils.AvatarURL(u.AvatarURL()))
}
}), nil
}
func (s *Session) Disconnect() error {
return s.Close()
return s.state.Close()
}
func (s *Session) AsSessionSaver() cchat.SessionSaver { return s.Instance }
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.AddHandler(func(*session.Closed) {
s.state.AddHandler(func(*session.Closed) {
container.SetServers(nil)
})
// Set the entire container again once reconnected.
s.AddHandler(func(*ningen.Connected) {
s.state.AddHandler(func(*ningen.Connected) {
s.servers(container)
})
@ -81,22 +91,26 @@ func (s *Session) Servers(container cchat.ServersContainer) error {
}
func (s *Session) servers(container cchat.ServersContainer) error {
// TODO: remove this once v2 is used, so we could swap it with a getter.
ready := s.state.Ready
switch {
// If the user has guild folders:
case len(s.Ready.Settings.GuildFolders) > 0:
case len(ready.Settings.GuildFolders) > 0:
// TODO: account for missing guilds.
var toplevels = make([]cchat.Server, 0, len(s.Ready.Settings.GuildFolders))
toplevels := make([]cchat.Server, 1, len(ready.Settings.GuildFolders)+1)
toplevels[0] = s.private
for _, guildFolder := range s.Ready.Settings.GuildFolders {
for _, guildFolder := range ready.Settings.GuildFolders {
// TODO: correct.
switch {
case guildFolder.ID != 0:
fallthrough
case len(guildFolder.GuildIDs) > 1:
toplevels = append(toplevels, folder.New(s.Instance, guildFolder))
toplevels = append(toplevels, folder.New(s.state, guildFolder))
case len(guildFolder.GuildIDs) == 1:
g, err := guild.NewFromID(s.Instance, guildFolder.GuildIDs[0])
g, err := guild.NewFromID(s.state, guildFolder.GuildIDs[0])
if err != nil {
continue
}
@ -108,11 +122,12 @@ func (s *Session) servers(container cchat.ServersContainer) error {
// If the user doesn't have guild folders but has sorted their guilds
// before:
case len(s.Ready.Settings.GuildPositions) > 0:
var guilds = make([]cchat.Server, 0, len(s.Ready.Settings.GuildPositions))
case len(ready.Settings.GuildPositions) > 0:
guilds := make([]cchat.Server, 1, len(ready.Settings.GuildPositions)+1)
guilds[0] = s.private
for _, id := range s.Ready.Settings.GuildPositions {
g, err := guild.NewFromID(s.Instance, id)
for _, id := range ready.Settings.GuildPositions {
g, err := guild.NewFromID(s.state, id)
if err != nil {
continue
}
@ -123,14 +138,16 @@ func (s *Session) servers(container cchat.ServersContainer) error {
// None of the above:
default:
g, err := s.Guilds()
g, err := s.state.Guilds()
if err != nil {
return err
}
var servers = make([]cchat.Server, len(g))
servers := make([]cchat.Server, len(g)+1)
servers[0] = s.private
for i := range g {
servers[i] = guild.New(s.Instance, &g[i])
servers[i+1] = guild.New(s.state, &g[i])
}
container.SetServers(servers)

View File

@ -10,7 +10,7 @@ func NewCancels() func(...func()) []func() {
}
// JoinCancels joins multiple cancel callbacks into one.
func JoinCancels(cancellers []func()) func() {
func JoinCancels(cancellers ...func()) func() {
return func() {
for _, c := range cancellers {
c()

View File

@ -187,8 +187,6 @@ func (r *Text) RenderNode(n ast.Node, enter bool) (ast.WalkStatus, error) {
return f(r, n, enter), nil
}
log.Println("unknown kind:", n.Kind())
switch n := n.(type) {
case *ast.Document:
case *ast.Paragraph: