Refactored to cchat v0.1.3
This commit is contained in:
parent
647c854d7b
commit
0f1cdafec6
72
category.go
72
category.go
|
@ -1,72 +0,0 @@
|
||||||
package discord
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Category struct {
|
|
||||||
id discord.ChannelID
|
|
||||||
guildID discord.GuildID
|
|
||||||
session *Session
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Server = (*Category)(nil)
|
|
||||||
_ cchat.ServerList = (*Category)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewCategory(s *Session, ch discord.Channel) *Category {
|
|
||||||
return &Category{
|
|
||||||
id: ch.ID,
|
|
||||||
guildID: ch.GuildID,
|
|
||||||
session: s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Category) ID() cchat.ID {
|
|
||||||
return c.id.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Category) Name() text.Rich {
|
|
||||||
t, err := c.session.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) Servers(container cchat.ServersContainer) error {
|
|
||||||
t, err := c.session.Channels(c.guildID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Failed to get channels")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out channels with this category ID.
|
|
||||||
var chs = filterAccessible(c.session, filterCategory(t, c.id))
|
|
||||||
|
|
||||||
sort.Slice(chs, func(i, j int) bool {
|
|
||||||
return chs[i].Position < chs[j].Position
|
|
||||||
})
|
|
||||||
|
|
||||||
var chv = make([]cchat.Server, len(chs))
|
|
||||||
for i := range chs {
|
|
||||||
c, err := NewChannel(c.session, chs[i])
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Failed to make channel %s: %v", chs[i].Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
chv[i] = c
|
|
||||||
}
|
|
||||||
|
|
||||||
container.SetServers(chv)
|
|
||||||
return nil
|
|
||||||
}
|
|
509
channel.go
509
channel.go
|
@ -1,509 +0,0 @@
|
||||||
package discord
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat-discord/segments"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/diamondburned/ningen/states/read"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func chGuildCheck(chType discord.ChannelType) bool {
|
|
||||||
switch chType {
|
|
||||||
case discord.GuildCategory, discord.GuildText:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterAccessible(s *Session, chs []discord.Channel) []discord.Channel {
|
|
||||||
filtered := chs[:0]
|
|
||||||
|
|
||||||
for _, ch := range chs {
|
|
||||||
p, err := s.Permissions(ch.ID, s.userID)
|
|
||||||
// Treat error as non-fatal and add the channel anyway.
|
|
||||||
if err != nil || p.Has(discord.PermissionViewChannel) {
|
|
||||||
filtered = append(filtered, ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterCategory(chs []discord.Channel, catID discord.ChannelID) []discord.Channel {
|
|
||||||
var filtered = chs[:0]
|
|
||||||
var catvalid = catID.IsValid()
|
|
||||||
|
|
||||||
for _, ch := range chs {
|
|
||||||
switch {
|
|
||||||
// If the given ID is not valid, then we look for channels with
|
|
||||||
// similarly invalid category IDs, because yes, Discord really sends
|
|
||||||
// inconsistent responses.
|
|
||||||
case !catvalid && !ch.CategoryID.IsValid():
|
|
||||||
fallthrough
|
|
||||||
// Basic comparison.
|
|
||||||
case ch.CategoryID == catID:
|
|
||||||
if chGuildCheck(ch.Type) {
|
|
||||||
filtered = append(filtered, ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
type Channel struct {
|
|
||||||
id discord.ChannelID
|
|
||||||
guildID discord.GuildID
|
|
||||||
session *Session
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Server = (*Channel)(nil)
|
|
||||||
_ cchat.ServerMessage = (*Channel)(nil)
|
|
||||||
_ cchat.ServerNickname = (*Channel)(nil)
|
|
||||||
_ cchat.ServerMessageEditor = (*Channel)(nil)
|
|
||||||
_ cchat.ServerMessageActioner = (*Channel)(nil)
|
|
||||||
_ cchat.ServerMessageBacklogger = (*Channel)(nil)
|
|
||||||
_ cchat.ServerMessageTypingIndicator = (*Channel)(nil)
|
|
||||||
_ cchat.ServerMessageUnreadIndicator = (*Channel)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewChannel(s *Session, ch discord.Channel) (cchat.Server, error) {
|
|
||||||
p, err := s.Permissions(ch.ID, s.userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Failed to get permission")
|
|
||||||
}
|
|
||||||
|
|
||||||
var channel = NewROChannel(s, ch)
|
|
||||||
if p.Has(discord.PermissionSendMessages) {
|
|
||||||
return NewSendableChannel(channel), nil
|
|
||||||
}
|
|
||||||
return channel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewROChannel creates a new read-only channel. This function is mainly used
|
|
||||||
// internally.
|
|
||||||
func NewROChannel(s *Session, ch discord.Channel) *Channel {
|
|
||||||
return &Channel{
|
|
||||||
id: ch.ID,
|
|
||||||
guildID: ch.GuildID,
|
|
||||||
session: s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// self does not do IO.
|
|
||||||
func (ch *Channel) self() (*discord.Channel, error) {
|
|
||||||
return ch.session.Store.Channel(ch.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// messages does not do IO.
|
|
||||||
func (ch *Channel) messages() ([]discord.Message, error) {
|
|
||||||
return ch.session.Store.Messages(ch.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) guild() (*discord.Guild, error) {
|
|
||||||
if ch.guildID.IsValid() {
|
|
||||||
return ch.session.Store.Guild(ch.guildID)
|
|
||||||
}
|
|
||||||
return nil, errors.New("channel not in a guild")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) ID() cchat.ID {
|
|
||||||
return ch.id.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) Name() text.Rich {
|
|
||||||
c, err := ch.self()
|
|
||||||
if err != nil {
|
|
||||||
return text.Rich{Content: ch.id.String()}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.NSFW {
|
|
||||||
return text.Rich{Content: "#!" + c.Name}
|
|
||||||
} else {
|
|
||||||
return text.Rich{Content: "#" + c.Name}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) (func(), error) {
|
|
||||||
// We don't have a nickname if we're not in a guild.
|
|
||||||
if !ch.guildID.IsValid() {
|
|
||||||
return func() {}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
state := ch.session.WithContext(ctx)
|
|
||||||
|
|
||||||
// MemberColor should fill up the state cache.
|
|
||||||
c, err := state.MemberColor(ch.guildID, ch.session.userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Failed to get self member color")
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := state.Member(ch.guildID, ch.session.userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Failed to get self member")
|
|
||||||
}
|
|
||||||
|
|
||||||
var rich = text.Rich{Content: m.User.Username}
|
|
||||||
if m.Nick != "" {
|
|
||||||
rich.Content = m.Nick
|
|
||||||
}
|
|
||||||
if c > 0 {
|
|
||||||
rich.Segments = []text.Segment{
|
|
||||||
segments.NewColored(len(rich.Content), c.Uint32()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
labeler.SetLabel(rich)
|
|
||||||
|
|
||||||
// Copy the user ID to use.
|
|
||||||
var selfID = m.User.ID
|
|
||||||
|
|
||||||
return ch.session.AddHandler(func(g *gateway.GuildMemberUpdateEvent) {
|
|
||||||
if g.GuildID != ch.guildID || g.User.ID != selfID {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var rich = text.Rich{Content: m.User.Username}
|
|
||||||
if m.Nick != "" {
|
|
||||||
rich.Content = m.Nick
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := ch.session.MemberColor(g.GuildID, selfID)
|
|
||||||
if err == nil {
|
|
||||||
rich.Segments = []text.Segment{
|
|
||||||
segments.NewColored(len(rich.Content), c.Uint32()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
labeler.SetLabel(rich)
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
|
|
||||||
state := ch.session.WithContext(ctx)
|
|
||||||
|
|
||||||
m, err := state.Messages(ch.id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var addcancel = newCancels()
|
|
||||||
|
|
||||||
var constructor func(discord.Message) cchat.MessageCreate
|
|
||||||
|
|
||||||
if ch.guildID.IsValid() {
|
|
||||||
// Create the backlog without any member information.
|
|
||||||
g, err := state.Guild(ch.guildID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Failed to get guild")
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor = func(m discord.Message) cchat.MessageCreate {
|
|
||||||
return NewBacklogMessage(m, ch.session, *g)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to typing events.
|
|
||||||
ch.session.MemberState.Subscribe(ch.guildID)
|
|
||||||
|
|
||||||
// Listen to new members before creating the backlog and requesting members.
|
|
||||||
addcancel(ch.session.AddHandler(func(c *gateway.GuildMembersChunkEvent) {
|
|
||||||
if c.GuildID != ch.guildID {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := ch.messages()
|
|
||||||
if err != nil {
|
|
||||||
// TODO: log
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
g, err := ch.guild()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop over all messages and replace the author. The latest
|
|
||||||
// messages are in front.
|
|
||||||
for _, msg := range m {
|
|
||||||
for _, member := range c.Members {
|
|
||||||
if msg.Author.ID != member.User.ID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ct.UpdateMessage(NewMessageUpdateAuthor(msg, member, *g, ch.session))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
constructor = func(m discord.Message) cchat.MessageCreate {
|
|
||||||
return NewDirectMessage(m, ch.session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only do all this if we even have any messages.
|
|
||||||
if len(m) > 0 {
|
|
||||||
// Sort messages chronologically using the ID so that the oldest messages
|
|
||||||
// (ones with the smallest snowflake) is in front.
|
|
||||||
sort.Slice(m, func(i, j int) bool { return m[i].ID < m[j].ID })
|
|
||||||
|
|
||||||
// Iterate from the earliest messages to the latest messages.
|
|
||||||
for _, m := range m {
|
|
||||||
ct.CreateMessage(constructor(m))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark this channel as read.
|
|
||||||
ch.session.ReadState.MarkRead(ch.id, m[len(m)-1].ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind the handler.
|
|
||||||
addcancel(
|
|
||||||
ch.session.AddHandler(func(m *gateway.MessageCreateEvent) {
|
|
||||||
if m.ChannelID == ch.id {
|
|
||||||
ct.CreateMessage(NewMessageCreate(m, ch.session))
|
|
||||||
ch.session.ReadState.MarkRead(ch.id, m.ID)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
ch.session.AddHandler(func(m *gateway.MessageUpdateEvent) {
|
|
||||||
// If the updated content is empty. TODO: add embed support.
|
|
||||||
if m.ChannelID == ch.id {
|
|
||||||
ct.UpdateMessage(NewMessageUpdateContent(m.Message, ch.session))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
ch.session.AddHandler(func(m *gateway.MessageDeleteEvent) {
|
|
||||||
if m.ChannelID == ch.id {
|
|
||||||
ct.DeleteMessage(NewHeaderDelete(m))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return joinCancels(addcancel()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) MessagesBefore(ctx context.Context, b cchat.ID, c cchat.MessagePrepender) error {
|
|
||||||
p, err := discord.ParseSnowflake(b)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Failed to parse snowflake")
|
|
||||||
}
|
|
||||||
|
|
||||||
s := ch.session.WithContext(ctx)
|
|
||||||
|
|
||||||
m, err := s.MessagesBefore(ch.id, discord.MessageID(p), uint(ch.session.MaxMessages()))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Failed to get messages")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the backlog without any member information.
|
|
||||||
g, err := s.Guild(ch.guildID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Failed to get guild")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range m {
|
|
||||||
// Discord sucks.
|
|
||||||
m.GuildID = ch.guildID
|
|
||||||
|
|
||||||
c.PrependMessage(NewBacklogMessage(m, ch.session, *g))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageEditable returns true if the given message ID belongs to the current
|
|
||||||
// user.
|
|
||||||
func (ch *Channel) MessageEditable(id string) bool {
|
|
||||||
s, err := discord.ParseSnowflake(id)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := ch.session.Store.Message(ch.id, discord.MessageID(s))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.Author.ID == ch.session.userID
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawMessageContent returns the raw message content from Discord.
|
|
||||||
func (ch *Channel) RawMessageContent(id string) (string, error) {
|
|
||||||
s, err := discord.ParseSnowflake(id)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrap(err, "Failed to parse ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := ch.session.Store.Message(ch.id, discord.MessageID(s))
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrap(err, "Failed to get the message")
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.Content, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditMessage edits the message to the given content string.
|
|
||||||
func (ch *Channel) EditMessage(id, content string) error {
|
|
||||||
s, err := discord.ParseSnowflake(id)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Failed to parse ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = ch.session.EditText(ch.id, discord.MessageID(s), content)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ActionDelete = "Delete"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrUnknownAction = errors.New("unknown message action")
|
|
||||||
|
|
||||||
func (ch *Channel) DoMessageAction(action, id string) error {
|
|
||||||
s, err := discord.ParseSnowflake(id)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Failed to parse ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch action {
|
|
||||||
case ActionDelete:
|
|
||||||
return ch.session.DeleteMessage(ch.id, discord.MessageID(s))
|
|
||||||
default:
|
|
||||||
return ErrUnknownAction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) MessageActions(id string) []string {
|
|
||||||
s, err := discord.ParseSnowflake(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := ch.session.Store.Message(ch.id, discord.MessageID(s))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current user.
|
|
||||||
u, err := ch.session.Store.Me()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can we have delete? We can if this is our own message.
|
|
||||||
var canDelete = m.Author.ID == u.ID
|
|
||||||
|
|
||||||
// We also can if we have the Manage Messages permission, which would allow
|
|
||||||
// us to delete others' messages.
|
|
||||||
if !canDelete {
|
|
||||||
canDelete = ch.canManageMessages(u.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if canDelete {
|
|
||||||
return []string{ActionDelete}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// canManageMessages returns whether or not the user is allowed to manage
|
|
||||||
// messages.
|
|
||||||
func (ch *Channel) canManageMessages(userID discord.UserID) bool {
|
|
||||||
// If we're not in a guild, then clearly we cannot.
|
|
||||||
if !ch.guildID.IsValid() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need the guild, member and channel to calculate the permission
|
|
||||||
// overrides.
|
|
||||||
|
|
||||||
g, err := ch.guild()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := ch.self()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := ch.session.Store.Member(ch.guildID, userID)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
p := discord.CalcOverwrites(*g, *c, *m)
|
|
||||||
// The Manage Messages permission allows the user to delete others'
|
|
||||||
// messages, so we'll return true if that is the case.
|
|
||||||
return p.Has(discord.PermissionManageMessages)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) Typing() error {
|
|
||||||
return ch.session.Typing(ch.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypingTimeout returns 10 seconds.
|
|
||||||
func (ch *Channel) TypingTimeout() time.Duration {
|
|
||||||
return 10 * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) TypingSubscribe(ti cchat.TypingIndicator) (func(), error) {
|
|
||||||
return ch.session.AddHandler(func(t *gateway.TypingStartEvent) {
|
|
||||||
// Ignore channel mismatch or if the typing event is ours.
|
|
||||||
if t.ChannelID != ch.id || t.UserID == ch.session.userID {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if typer, err := NewTyper(ch.session, t); err == nil {
|
|
||||||
ti.AddTyper(typer)
|
|
||||||
}
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// muted returns if this channel is muted. This includes the channel's category
|
|
||||||
// and guild.
|
|
||||||
func (ch *Channel) muted() bool {
|
|
||||||
return (ch.guildID.IsValid() && ch.session.MutedState.Guild(ch.guildID, false)) ||
|
|
||||||
ch.session.MutedState.Channel(ch.id) ||
|
|
||||||
ch.session.MutedState.Category(ch.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) UnreadIndicate(indicator cchat.UnreadIndicator) (func(), error) {
|
|
||||||
if rs := ch.session.ReadState.FindLast(ch.id); rs != nil {
|
|
||||||
c, err := ch.self()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Failed to get self channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.LastMessageID > rs.LastMessageID && !ch.muted() {
|
|
||||||
indicator.SetUnread(true, rs.MentionCount > 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ch.session.ReadState.OnUpdate(func(ev *read.UpdateEvent) {
|
|
||||||
if ch.id == ev.ChannelID && !ch.muted() {
|
|
||||||
indicator.SetUnread(ev.Unread, ev.MentionCount > 0)
|
|
||||||
}
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCancels() func(...func()) []func() {
|
|
||||||
var cancels []func()
|
|
||||||
return func(appended ...func()) []func() {
|
|
||||||
cancels = append(cancels, appended...)
|
|
||||||
return cancels
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinCancels(cancellers []func()) func() {
|
|
||||||
return func() {
|
|
||||||
for _, c := range cancellers {
|
|
||||||
c()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,345 +0,0 @@
|
||||||
package discord
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat-discord/segments"
|
|
||||||
"github.com/diamondburned/cchat-discord/urlutils"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/diamondburned/ningen/states/member"
|
|
||||||
)
|
|
||||||
|
|
||||||
func seekPrevGroup(l *member.List, ix int) (item, group gateway.GuildMemberListOpItem) {
|
|
||||||
l.ViewItems(func(items []gateway.GuildMemberListOpItem) {
|
|
||||||
// Bound check.
|
|
||||||
if ix >= len(items) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
item = items[ix]
|
|
||||||
|
|
||||||
// Search backwards.
|
|
||||||
for i := ix; i >= 0; i-- {
|
|
||||||
if items[i].Group != nil {
|
|
||||||
group = items[i]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) ListMembers(ctx context.Context, c cchat.MemberListContainer) (func(), error) {
|
|
||||||
if !ch.guildID.IsValid() {
|
|
||||||
return func() {}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel := ch.session.AddHandler(func(u *gateway.GuildMemberListUpdate) {
|
|
||||||
l, err := ch.session.MemberState.GetMemberList(ch.guildID, ch.id)
|
|
||||||
if err != nil {
|
|
||||||
return // wat
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.GuildID() != u.GuildID || l.ID() != u.ID {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ev := range u.Ops {
|
|
||||||
switch ev.Op {
|
|
||||||
case "SYNC":
|
|
||||||
ch.checkSync(c)
|
|
||||||
|
|
||||||
case "INSERT", "UPDATE":
|
|
||||||
item, group := seekPrevGroup(l, ev.Index)
|
|
||||||
if item.Member != nil && group.Group != nil {
|
|
||||||
c.SetMember(group.Group.ID, NewListMember(ch, item))
|
|
||||||
ch.flushMemberGroups(l, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "DELETE":
|
|
||||||
_, group := seekPrevGroup(l, ev.Index-1)
|
|
||||||
if group.Group != nil && ev.Item.Member != nil {
|
|
||||||
c.RemoveMember(group.Group.ID, ev.Item.Member.User.ID.String())
|
|
||||||
ch.flushMemberGroups(l, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ch.checkSync(c)
|
|
||||||
|
|
||||||
return cancel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) checkSync(c cchat.MemberListContainer) {
|
|
||||||
l, err := ch.session.MemberState.GetMemberList(ch.guildID, ch.id)
|
|
||||||
if err != nil {
|
|
||||||
ch.session.MemberState.RequestMemberList(ch.guildID, ch.id, 0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ch.flushMemberGroups(l, c)
|
|
||||||
|
|
||||||
l.ViewItems(func(items []gateway.GuildMemberListOpItem) {
|
|
||||||
var group gateway.GuildMemberListGroup
|
|
||||||
|
|
||||||
for _, item := range items {
|
|
||||||
switch {
|
|
||||||
case item.Group != nil:
|
|
||||||
group = *item.Group
|
|
||||||
|
|
||||||
case item.Member != nil:
|
|
||||||
c.SetMember(group.ID, NewListMember(ch, item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Channel) flushMemberGroups(l *member.List, c cchat.MemberListContainer) {
|
|
||||||
l.ViewGroups(func(groups []gateway.GuildMemberListGroup) {
|
|
||||||
var sections = make([]cchat.MemberListSection, len(groups))
|
|
||||||
for i, group := range groups {
|
|
||||||
sections[i] = NewListSection(l.ID(), ch, group)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetSections(sections)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListMember struct {
|
|
||||||
// Keep stateful references to do on-demand loading.
|
|
||||||
channel *Channel
|
|
||||||
|
|
||||||
// constant states
|
|
||||||
userID discord.UserID
|
|
||||||
origName string // use if cache is stale
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.ListMember = (*ListMember)(nil)
|
|
||||||
_ cchat.Icon = (*ListMember)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewListMember creates a new list member. it.Member must not be nil.
|
|
||||||
func NewListMember(ch *Channel, it gateway.GuildMemberListOpItem) *ListMember {
|
|
||||||
return &ListMember{
|
|
||||||
channel: ch,
|
|
||||||
userID: it.Member.User.ID,
|
|
||||||
origName: it.Member.User.Username,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListMember) ID() cchat.ID {
|
|
||||||
return l.userID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListMember) Name() text.Rich {
|
|
||||||
g, err := l.channel.guild()
|
|
||||||
if err != nil {
|
|
||||||
return text.Plain(l.origName)
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := l.channel.session.Member(l.channel.guildID, l.userID)
|
|
||||||
if err != nil {
|
|
||||||
return text.Plain(l.origName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = m.User.Username
|
|
||||||
if m.Nick != "" {
|
|
||||||
name = m.Nick
|
|
||||||
}
|
|
||||||
|
|
||||||
mention := segments.MemberSegment(0, len(name), *g, *m)
|
|
||||||
mention.WithState(l.channel.session.State)
|
|
||||||
|
|
||||||
var txt = text.Rich{
|
|
||||||
Content: name,
|
|
||||||
Segments: []text.Segment{mention},
|
|
||||||
}
|
|
||||||
|
|
||||||
if c := discord.MemberColor(*g, *m); c != discord.DefaultMemberColor {
|
|
||||||
txt.Segments = append(txt.Segments, segments.NewColored(len(name), uint32(c)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return txt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListMember) Icon(ctx context.Context, c cchat.IconContainer) (func(), error) {
|
|
||||||
m, err := l.channel.session.Member(l.channel.guildID, l.userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetIcon(urlutils.AvatarURL(m.User.AvatarURL()))
|
|
||||||
|
|
||||||
return func() {}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListMember) Status() cchat.UserStatus {
|
|
||||||
p, err := l.channel.session.State.Presence(l.channel.guildID, l.userID)
|
|
||||||
if err != nil {
|
|
||||||
return cchat.UnknownStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
switch p.Status {
|
|
||||||
case discord.OnlineStatus:
|
|
||||||
return cchat.OnlineStatus
|
|
||||||
case discord.DoNotDisturbStatus:
|
|
||||||
return cchat.BusyStatus
|
|
||||||
case discord.IdleStatus:
|
|
||||||
return cchat.AwayStatus
|
|
||||||
case discord.OfflineStatus, discord.InvisibleStatus:
|
|
||||||
return cchat.OfflineStatus
|
|
||||||
default:
|
|
||||||
return cchat.UnknownStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListMember) Secondary() text.Rich {
|
|
||||||
p, err := l.channel.session.State.Presence(l.channel.guildID, l.userID)
|
|
||||||
if err != nil {
|
|
||||||
return text.Plain("")
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Game != nil {
|
|
||||||
return formatSmallActivity(*p.Game)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.Activities) > 0 {
|
|
||||||
return formatSmallActivity(p.Activities[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.Plain("")
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatSmallActivity(ac discord.Activity) text.Rich {
|
|
||||||
switch ac.Type {
|
|
||||||
case discord.GameActivity:
|
|
||||||
return text.Plain(fmt.Sprintf("Playing %s", ac.Name))
|
|
||||||
|
|
||||||
case discord.ListeningActivity:
|
|
||||||
return text.Plain(fmt.Sprintf("Listening to %s", ac.Name))
|
|
||||||
|
|
||||||
case discord.StreamingActivity:
|
|
||||||
return text.Plain(fmt.Sprintf("Streaming on %s", ac.Name))
|
|
||||||
|
|
||||||
case discord.CustomActivity:
|
|
||||||
var status strings.Builder
|
|
||||||
var segmts []text.Segment
|
|
||||||
|
|
||||||
if ac.Emoji != nil {
|
|
||||||
if !ac.Emoji.ID.IsValid() {
|
|
||||||
status.WriteString(ac.Emoji.Name)
|
|
||||||
status.WriteByte(' ')
|
|
||||||
} else {
|
|
||||||
segmts = append(segmts, segments.EmojiSegment{
|
|
||||||
Start: status.Len(),
|
|
||||||
Name: ac.Emoji.Name,
|
|
||||||
EmojiURL: ac.Emoji.EmojiURL() + "?size=64",
|
|
||||||
Large: ac.State == "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status.WriteString(ac.State)
|
|
||||||
|
|
||||||
return text.Rich{
|
|
||||||
Content: status.String(),
|
|
||||||
Segments: segmts,
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return text.Rich{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListSection struct {
|
|
||||||
// constant states
|
|
||||||
listID string
|
|
||||||
id string // roleID or online or offline
|
|
||||||
name string
|
|
||||||
total int
|
|
||||||
|
|
||||||
channel *Channel
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.MemberListSection = (*ListSection)(nil)
|
|
||||||
_ cchat.MemberListDynamicSection = (*ListSection)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewListSection(listID string, ch *Channel, group gateway.GuildMemberListGroup) *ListSection {
|
|
||||||
var name string
|
|
||||||
|
|
||||||
switch group.ID {
|
|
||||||
case "online":
|
|
||||||
name = "Online"
|
|
||||||
case "offline":
|
|
||||||
name = "Offline"
|
|
||||||
default:
|
|
||||||
p, err := discord.ParseSnowflake(group.ID)
|
|
||||||
if err != nil {
|
|
||||||
name = group.ID
|
|
||||||
} else {
|
|
||||||
r, err := ch.session.Role(ch.guildID, discord.RoleID(p))
|
|
||||||
if err != nil {
|
|
||||||
name = fmt.Sprintf("<@#%s>", p.String())
|
|
||||||
} else {
|
|
||||||
name = r.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ListSection{
|
|
||||||
listID: listID,
|
|
||||||
channel: ch,
|
|
||||||
id: group.ID,
|
|
||||||
name: name,
|
|
||||||
total: int(group.Count),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ListSection) ID() cchat.ID {
|
|
||||||
return s.id
|
|
||||||
// return fmt.Sprintf("%s-%s", s.listID, s.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ListSection) Name() text.Rich {
|
|
||||||
return text.Rich{Content: s.name}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ListSection) Total() int {
|
|
||||||
return s.total
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: document that Load{More,Less} works more like a shifting window.
|
|
||||||
|
|
||||||
func (s *ListSection) LoadMore() bool {
|
|
||||||
// This variable is here purely to make lines shorter.
|
|
||||||
var memstate = s.channel.session.MemberState
|
|
||||||
|
|
||||||
chunk := memstate.GetMemberListChunk(s.channel.guildID, s.channel.id)
|
|
||||||
if chunk < 0 {
|
|
||||||
chunk = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return memstate.RequestMemberList(s.channel.guildID, s.channel.id, chunk) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ListSection) LoadLess() bool {
|
|
||||||
var memstate = s.channel.session.MemberState
|
|
||||||
|
|
||||||
chunk := memstate.GetMemberListChunk(s.channel.guildID, s.channel.id)
|
|
||||||
if chunk <= 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
memstate.RequestMemberList(s.channel.guildID, s.channel.id, chunk-1)
|
|
||||||
return true
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
package discord
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/diamondburned/arikawa/api"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SendableChannel struct {
|
|
||||||
Channel
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSendableChannel creates a sendable channel. This function is mainly used
|
|
||||||
// internally
|
|
||||||
func NewSendableChannel(ch *Channel) *SendableChannel {
|
|
||||||
return &SendableChannel{*ch}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.ServerMessageSender = (*SendableChannel)(nil)
|
|
||||||
_ cchat.ServerMessageSendCompleter = (*SendableChannel)(nil)
|
|
||||||
_ cchat.ServerMessageAttachmentSender = (*SendableChannel)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ch *SendableChannel) SendMessage(msg cchat.SendableMessage) error {
|
|
||||||
var send = api.SendMessageData{Content: msg.Content()}
|
|
||||||
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
|
||||||
send.Nonce = noncer.Nonce()
|
|
||||||
}
|
|
||||||
if attcher, ok := msg.(cchat.SendableMessageAttachments); ok {
|
|
||||||
send.Files = addAttachments(attcher.Attachments())
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := ch.session.SendMessageComplex(ch.id, send)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *SendableChannel) SendAttachments(atts []cchat.MessageAttachment) error {
|
|
||||||
_, err := ch.session.SendMessageComplex(ch.id, api.SendMessageData{
|
|
||||||
Files: addAttachments(atts),
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func addAttachments(atts []cchat.MessageAttachment) []api.SendMessageFile {
|
|
||||||
var files = make([]api.SendMessageFile, len(atts))
|
|
||||||
for i, a := range atts {
|
|
||||||
files[i] = api.SendMessageFile{
|
|
||||||
Name: a.Name,
|
|
||||||
Reader: a,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompleteMessage implements message input completion capability for Discord.
|
|
||||||
// This method supports user mentions, channel mentions and emojis.
|
|
||||||
//
|
|
||||||
// For the individual implementations, refer to channel_completion.go.
|
|
||||||
func (ch *SendableChannel) CompleteMessage(words []string, i int) (entries []cchat.CompletionEntry) {
|
|
||||||
var word = words[i]
|
|
||||||
// Word should have at least a character for the char check.
|
|
||||||
if len(word) < 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch word[0] {
|
|
||||||
case '@':
|
|
||||||
return ch.completeMentions(word[1:])
|
|
||||||
case '#':
|
|
||||||
return ch.completeChannels(word[1:])
|
|
||||||
case ':':
|
|
||||||
return ch.completeEmojis(word[1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package discord
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/session"
|
||||||
|
"github.com/diamondburned/cchat/services"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
services.RegisterService(&Service{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
||||||
|
var ErrInvalidSession = errors.New("invalid session")
|
||||||
|
|
||||||
|
type Service struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.Iconer = (*Service)(nil)
|
||||||
|
_ cchat.Service = (*Service)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (Service) Name() text.Rich {
|
||||||
|
return text.Rich{Content: "Discord"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIconer returns true.
|
||||||
|
func (Service) IsIconer() bool { return true }
|
||||||
|
|
||||||
|
func (Service) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||||
|
iconer.SetIcon("https://raw.githubusercontent.com/" +
|
||||||
|
"diamondburned/cchat-discord/himearikawa/discord_logo.png")
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Service) Authenticate() cchat.Authenticator {
|
||||||
|
return &Authenticator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) RestoreSession(data map[string]string) (cchat.Session, error) {
|
||||||
|
tk, ok := data["token"]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrInvalidSession
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.NewFromToken(tk)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Authenticator struct{}
|
||||||
|
|
||||||
|
var _ cchat.Authenticator = (*Authenticator)(nil)
|
||||||
|
|
||||||
|
func (*Authenticator) AuthenticateForm() []cchat.AuthenticateEntry {
|
||||||
|
// TODO: username, password and 2FA
|
||||||
|
return []cchat.AuthenticateEntry{
|
||||||
|
{
|
||||||
|
Name: "Token",
|
||||||
|
Secret: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "(or) Username",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Authenticator) Authenticate(form []string) (cchat.Session, error) {
|
||||||
|
switch {
|
||||||
|
case form[0] != "": // Token
|
||||||
|
return session.NewFromToken(form[0])
|
||||||
|
case form[1] != "": // Username
|
||||||
|
return nil, errors.New("username sign-in is not supported yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("malformed authentication form")
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -4,7 +4,7 @@ go 1.14
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/diamondburned/arikawa v1.3.0
|
github.com/diamondburned/arikawa v1.3.0
|
||||||
github.com/diamondburned/cchat v0.0.49
|
github.com/diamondburned/cchat v0.1.3
|
||||||
github.com/diamondburned/ningen v0.1.1-0.20200820222640-35796f938a58
|
github.com/diamondburned/ningen v0.1.1-0.20200820222640-35796f938a58
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/go-test/deep v1.0.6
|
github.com/go-test/deep v1.0.6
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -71,6 +71,14 @@ github.com/diamondburned/cchat v0.0.48 h1:MAzGzKY20JBh/LnirOZVPwbMq07xfqu4Lb4XsV
|
||||||
github.com/diamondburned/cchat v0.0.48/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat v0.0.48/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
github.com/diamondburned/cchat v0.0.49 h1:zP6QvjdRU3UqDZt3rEqjkR/5M68XRVms7htHfE9tLOc=
|
github.com/diamondburned/cchat v0.0.49 h1:zP6QvjdRU3UqDZt3rEqjkR/5M68XRVms7htHfE9tLOc=
|
||||||
github.com/diamondburned/cchat v0.0.49/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat v0.0.49/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
|
github.com/diamondburned/cchat v0.1.0 h1:TJiMdKFd1mijQOO1KSp35PJMvW+jiif5Go4QmoIhH9I=
|
||||||
|
github.com/diamondburned/cchat v0.1.0/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
|
github.com/diamondburned/cchat v0.1.1 h1:tx130Vx0bvLQvQxyOJbhvPJ85qoOOs5ZhJVXDDIh7eU=
|
||||||
|
github.com/diamondburned/cchat v0.1.1/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
|
github.com/diamondburned/cchat v0.1.2 h1:/9/xtHeifirMHiHsf/acL23UPZuS2YdzqWMMR5+sUPU=
|
||||||
|
github.com/diamondburned/cchat v0.1.2/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
|
github.com/diamondburned/cchat v0.1.3 h1:4xq8Tc+U0OUf2Vr6s8Igb5iADmeJ9oM1Db+M6zF/PDQ=
|
||||||
|
github.com/diamondburned/cchat v0.1.3/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I=
|
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.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
|
||||||
github.com/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M=
|
github.com/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M=
|
||||||
|
|
189
guild.go
189
guild.go
|
@ -1,189 +0,0 @@
|
||||||
package discord
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat-discord/segments"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GuildFolder struct {
|
|
||||||
gateway.GuildFolder
|
|
||||||
session *Session
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Server = (*Guild)(nil)
|
|
||||||
_ cchat.ServerList = (*Guild)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewGuildFolder(s *Session, gf gateway.GuildFolder) *GuildFolder {
|
|
||||||
// Name should never be empty.
|
|
||||||
if gf.Name == "" {
|
|
||||||
var names = make([]string, 0, len(gf.GuildIDs))
|
|
||||||
|
|
||||||
for _, id := range gf.GuildIDs {
|
|
||||||
if g, _ := s.Store.Guild(id); g != nil {
|
|
||||||
names = append(names, g.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gf.Name = strings.Join(names, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &GuildFolder{
|
|
||||||
GuildFolder: gf,
|
|
||||||
session: 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.
|
|
||||||
segments.NewColored(len(name.Content), gf.GuildFolder.Color.Uint32()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
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.session.Guild(id)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
servers = append(servers, NewGuild(gf.session, g))
|
|
||||||
}
|
|
||||||
|
|
||||||
container.SetServers(servers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Guild struct {
|
|
||||||
id discord.GuildID
|
|
||||||
session *Session
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Icon = (*Guild)(nil)
|
|
||||||
_ cchat.Server = (*Guild)(nil)
|
|
||||||
_ cchat.ServerList = (*Guild)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewGuild(s *Session, g *discord.Guild) *Guild {
|
|
||||||
return &Guild{
|
|
||||||
id: g.ID,
|
|
||||||
session: s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGuildFromID(s *Session, gID discord.GuildID) (*Guild, error) {
|
|
||||||
g, err := s.Guild(gID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewGuild(s, g), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Guild) self(ctx context.Context) (*discord.Guild, error) {
|
|
||||||
return g.session.WithContext(ctx).Guild(g.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Guild) selfState() (*discord.Guild, error) {
|
|
||||||
return g.session.Store.Guild(g.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Guild) ID() cchat.ID {
|
|
||||||
return g.id.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Guild) Name() text.Rich {
|
|
||||||
s, err := g.selfState()
|
|
||||||
if err != nil {
|
|
||||||
// This shouldn't happen.
|
|
||||||
return text.Rich{Content: g.id.String()}
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.Rich{Content: s.Name}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
|
||||||
s, err := g.self(ctx)
|
|
||||||
if err != nil {
|
|
||||||
// This shouldn't happen.
|
|
||||||
return nil, errors.Wrap(err, "Failed to get guild")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for comparison.
|
|
||||||
var hash = s.Icon
|
|
||||||
if hash != "" {
|
|
||||||
iconer.SetIcon(AvatarURL(s.IconURL()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return g.session.AddHandler(func(g *gateway.GuildUpdateEvent) {
|
|
||||||
if g.Icon != hash {
|
|
||||||
hash = g.Icon
|
|
||||||
iconer.SetIcon(AvatarURL(s.IconURL()))
|
|
||||||
}
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Guild) Servers(container cchat.ServersContainer) error {
|
|
||||||
c, err := g.session.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 = filterAccessible(g.session, 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
|
|
||||||
})
|
|
||||||
|
|
||||||
var chs = make([]cchat.Server, 0, len(toplevels))
|
|
||||||
|
|
||||||
for _, ch := range toplevels {
|
|
||||||
switch ch.Type {
|
|
||||||
case discord.GuildCategory:
|
|
||||||
chs = append(chs, NewCategory(g.session, ch))
|
|
||||||
case discord.GuildText:
|
|
||||||
c, err := NewChannel(g.session, ch)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Failed to make channel %q: %v", ch.Name, err)
|
|
||||||
}
|
|
||||||
chs = append(chs, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
container.SetServers(chs)
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
package category
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ChGuildCheck(chType discord.ChannelType) bool {
|
||||||
|
switch chType {
|
||||||
|
case discord.GuildCategory, discord.GuildText:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FilterAccessible(s *state.Instance, chs []discord.Channel) []discord.Channel {
|
||||||
|
filtered := chs[:0]
|
||||||
|
|
||||||
|
for _, ch := range chs {
|
||||||
|
p, err := s.Permissions(ch.ID, s.UserID)
|
||||||
|
// Treat error as non-fatal and add the channel anyway.
|
||||||
|
if err != nil || p.Has(discord.PermissionViewChannel) {
|
||||||
|
filtered = append(filtered, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func FilterCategory(chs []discord.Channel, catID discord.ChannelID) []discord.Channel {
|
||||||
|
var filtered = chs[:0]
|
||||||
|
var catvalid = catID.IsValid()
|
||||||
|
|
||||||
|
for _, ch := range chs {
|
||||||
|
switch {
|
||||||
|
// If the given ID is not valid, then we look for channels with
|
||||||
|
// similarly invalid category IDs, because yes, Discord really sends
|
||||||
|
// inconsistent responses.
|
||||||
|
case !catvalid && !ch.CategoryID.IsValid():
|
||||||
|
fallthrough
|
||||||
|
// Basic comparison.
|
||||||
|
case ch.CategoryID == catID:
|
||||||
|
if ChGuildCheck(ch.Type) {
|
||||||
|
filtered = append(filtered, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
id discord.ChannelID
|
||||||
|
guildID discord.GuildID
|
||||||
|
state *state.Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.Server = (*Category)(nil)
|
||||||
|
_ cchat.Lister = (*Category)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(s *state.Instance, ch discord.Channel) *Category {
|
||||||
|
return &Category{
|
||||||
|
id: ch.ID,
|
||||||
|
guildID: ch.GuildID,
|
||||||
|
state: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) IsLister() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Category) Servers(container cchat.ServersContainer) error {
|
||||||
|
t, err := c.state.Channels(c.guildID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to get channels")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out channels with this category ID.
|
||||||
|
var chs = FilterAccessible(c.state, FilterCategory(t, c.id))
|
||||||
|
|
||||||
|
sort.Slice(chs, func(i, j int) bool {
|
||||||
|
return chs[i].Position < chs[j].Position
|
||||||
|
})
|
||||||
|
|
||||||
|
var chv = make([]cchat.Server, len(chs))
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
chv[i] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
container.SetServers(chv)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cchat.Actioner = (*Channel)(nil)
|
||||||
|
|
||||||
|
// IsActioner returns true.
|
||||||
|
func (ch *Channel) IsActioner() bool { return true }
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionDelete = "Delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrUnknownAction = errors.New("unknown message action")
|
||||||
|
|
||||||
|
func (ch *Channel) DoMessageAction(action, id string) error {
|
||||||
|
s, err := discord.ParseSnowflake(id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to parse ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case ActionDelete:
|
||||||
|
return ch.state.DeleteMessage(ch.id, discord.MessageID(s))
|
||||||
|
default:
|
||||||
|
return ErrUnknownAction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) MessageActions(id string) []string {
|
||||||
|
s, err := discord.ParseSnowflake(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ch.state.Store.Message(ch.id, discord.MessageID(s))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current user.
|
||||||
|
u, err := ch.state.Store.Me()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can we have delete? We can if this is our own message.
|
||||||
|
var canDelete = m.Author.ID == u.ID
|
||||||
|
|
||||||
|
// We also can if we have the Manage Messages permission, which would allow
|
||||||
|
// us to delete others' messages.
|
||||||
|
if !canDelete {
|
||||||
|
canDelete = ch.canManageMessages(u.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if canDelete {
|
||||||
|
return []string{ActionDelete}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// canManageMessages returns whether or not the user is allowed to manage
|
||||||
|
// messages.
|
||||||
|
func (ch *Channel) canManageMessages(userID discord.UserID) bool {
|
||||||
|
// If we're not in a guild, then clearly we cannot.
|
||||||
|
if !ch.guildID.IsValid() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need the guild, member and channel to calculate the permission
|
||||||
|
// overrides.
|
||||||
|
|
||||||
|
g, err := ch.guild()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := ch.self()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ch.state.Store.Member(ch.guildID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
p := discord.CalcOverwrites(*g, *c, *m)
|
||||||
|
// The Manage Messages permission allows the user to delete others'
|
||||||
|
// messages, so we'll return true if that is the case.
|
||||||
|
return p.Has(discord.PermissionManageMessages)
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/message"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cchat.Backlogger = (*Channel)(nil)
|
||||||
|
|
||||||
|
// IsBacklogger returns true if the current user can read the channel's message
|
||||||
|
// history.
|
||||||
|
func (ch *Channel) IsBacklogger() bool {
|
||||||
|
p, err := ch.state.StateOnly().Permissions(ch.id, ch.state.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Has(discord.PermissionViewChannel) && p.Has(discord.PermissionReadMessageHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) MessagesBefore(ctx context.Context, b cchat.ID, c cchat.MessagePrepender) error {
|
||||||
|
p, err := discord.ParseSnowflake(b)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to parse snowflake")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := ch.state.WithContext(ctx)
|
||||||
|
|
||||||
|
m, err := s.MessagesBefore(ch.id, discord.MessageID(p), uint(ch.state.MaxMessages()))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to get messages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the backlog without any member information.
|
||||||
|
g, err := s.Guild(ch.guildID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to get guild")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range m {
|
||||||
|
// Discord sucks.
|
||||||
|
m.GuildID = ch.guildID
|
||||||
|
|
||||||
|
c.PrependMessage(message.NewBacklogMessage(m, ch.state, *g))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
id discord.ChannelID
|
||||||
|
guildID discord.GuildID
|
||||||
|
state *state.Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Server = (*Channel)(nil)
|
||||||
|
|
||||||
|
func New(s *state.Instance, ch discord.Channel) (cchat.Server, 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{
|
||||||
|
id: ch.ID,
|
||||||
|
guildID: ch.GuildID,
|
||||||
|
state: s,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// self does not do IO.
|
||||||
|
func (ch *Channel) self() (*discord.Channel, error) {
|
||||||
|
return ch.state.Store.Channel(ch.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// messages does not do IO.
|
||||||
|
func (ch *Channel) messages() ([]discord.Message, error) {
|
||||||
|
return ch.state.Store.Messages(ch.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) guild() (*discord.Guild, error) {
|
||||||
|
if ch.guildID.IsValid() {
|
||||||
|
return ch.state.Store.Guild(ch.guildID)
|
||||||
|
}
|
||||||
|
return nil, errors.New("channel not in a guild")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) ID() cchat.ID {
|
||||||
|
return ch.id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) Name() text.Rich {
|
||||||
|
c, err := ch.self()
|
||||||
|
if err != nil {
|
||||||
|
return text.Rich{Content: ch.id.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.NSFW {
|
||||||
|
return text.Rich{Content: "#!" + c.Name}
|
||||||
|
} else {
|
||||||
|
return text.Rich{Content: "#" + c.Name}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,61 @@
|
||||||
package discord
|
package channel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/discord/message"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MaxCompletion = 15
|
const MaxCompletion = 15
|
||||||
|
|
||||||
func completionUserEntry(s *Session, u discord.User, g *discord.Guild) cchat.CompletionEntry {
|
var _ cchat.MessageCompleter = (*Channel)(nil)
|
||||||
|
|
||||||
|
// IsMessageCompleter returns true if the user can send messages in this
|
||||||
|
// channel.
|
||||||
|
func (ch *Channel) IsMessageCompleter() bool {
|
||||||
|
p, err := ch.state.StateOnly().Permissions(ch.id, ch.state.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Has(discord.PermissionSendMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteMessage implements message input completion capability for Discord.
|
||||||
|
// This method supports user mentions, channel mentions and emojis.
|
||||||
|
//
|
||||||
|
// For the individual implementations, refer to channel_completion.go.
|
||||||
|
func (ch *Channel) CompleteMessage(words []string, i int) (entries []cchat.CompletionEntry) {
|
||||||
|
var word = words[i]
|
||||||
|
// Word should have at least a character for the char check.
|
||||||
|
if len(word) < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch word[0] {
|
||||||
|
case '@':
|
||||||
|
return ch.completeMentions(word[1:])
|
||||||
|
case '#':
|
||||||
|
return ch.completeChannels(word[1:])
|
||||||
|
case ':':
|
||||||
|
return ch.completeEmojis(word[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func completionUser(s *state.Instance, u discord.User, g *discord.Guild) cchat.CompletionEntry {
|
||||||
if g != nil {
|
if g != nil {
|
||||||
m, err := s.Store.Member(g.ID, u.ID)
|
m, err := s.Store.Member(g.ID, u.ID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return cchat.CompletionEntry{
|
return cchat.CompletionEntry{
|
||||||
Raw: u.Mention(),
|
Raw: u.Mention(),
|
||||||
Text: RenderMemberName(*m, *g, s),
|
Text: message.RenderMemberName(*m, *g, s),
|
||||||
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
|
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
|
||||||
IconURL: u.AvatarURL(),
|
IconURL: u.AvatarURL(),
|
||||||
}
|
}
|
||||||
|
@ -32,7 +70,7 @@ func completionUserEntry(s *Session, u discord.User, g *discord.Guild) cchat.Com
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *SendableChannel) completeMentions(word string) (entries []cchat.CompletionEntry) {
|
func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntry) {
|
||||||
// If there is no input, then we should grab the latest messages.
|
// If there is no input, then we should grab the latest messages.
|
||||||
if word == "" {
|
if word == "" {
|
||||||
msgs, _ := ch.messages()
|
msgs, _ := ch.messages()
|
||||||
|
@ -50,7 +88,7 @@ func (ch *SendableChannel) completeMentions(word string) (entries []cchat.Comple
|
||||||
|
|
||||||
// Record the current author and add the entry to the list.
|
// Record the current author and add the entry to the list.
|
||||||
authors[msg.Author.ID] = struct{}{}
|
authors[msg.Author.ID] = struct{}{}
|
||||||
entries = append(entries, completionUserEntry(ch.session, msg.Author, g))
|
entries = append(entries, completionUser(ch.state, msg.Author, g))
|
||||||
|
|
||||||
if len(entries) >= MaxCompletion {
|
if len(entries) >= MaxCompletion {
|
||||||
return
|
return
|
||||||
|
@ -89,7 +127,7 @@ func (ch *SendableChannel) completeMentions(word string) (entries []cchat.Comple
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're in a guild, then we should search for (all) members.
|
// If we're in a guild, then we should search for (all) members.
|
||||||
m, merr := ch.session.Store.Members(ch.guildID)
|
m, merr := ch.state.Store.Members(ch.guildID)
|
||||||
g, gerr := ch.guild()
|
g, gerr := ch.guild()
|
||||||
|
|
||||||
if merr != nil || gerr != nil {
|
if merr != nil || gerr != nil {
|
||||||
|
@ -99,7 +137,7 @@ func (ch *SendableChannel) completeMentions(word string) (entries []cchat.Comple
|
||||||
// If we couldn't find any members, then we can request Discord to
|
// If we couldn't find any members, then we can request Discord to
|
||||||
// search for them.
|
// search for them.
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
ch.session.MemberState.SearchMember(ch.guildID, word)
|
ch.state.MemberState.SearchMember(ch.guildID, word)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +145,7 @@ func (ch *SendableChannel) completeMentions(word string) (entries []cchat.Comple
|
||||||
if contains(match, mem.User.Username, mem.Nick) {
|
if contains(match, mem.User.Username, mem.Nick) {
|
||||||
entries = append(entries, cchat.CompletionEntry{
|
entries = append(entries, cchat.CompletionEntry{
|
||||||
Raw: mem.User.Mention(),
|
Raw: mem.User.Mention(),
|
||||||
Text: RenderMemberName(mem, *g, ch.session),
|
Text: message.RenderMemberName(mem, *g, ch.state),
|
||||||
Secondary: text.Rich{Content: mem.User.Username + "#" + mem.User.Discriminator},
|
Secondary: text.Rich{Content: mem.User.Username + "#" + mem.User.Discriminator},
|
||||||
IconURL: mem.User.AvatarURL(),
|
IconURL: mem.User.AvatarURL(),
|
||||||
})
|
})
|
||||||
|
@ -120,7 +158,7 @@ func (ch *SendableChannel) completeMentions(word string) (entries []cchat.Comple
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *SendableChannel) completeChannels(word string) (entries []cchat.CompletionEntry) {
|
func (ch *Channel) completeChannels(word string) (entries []cchat.CompletionEntry) {
|
||||||
// Ignore if empty word.
|
// Ignore if empty word.
|
||||||
if word == "" {
|
if word == "" {
|
||||||
return
|
return
|
||||||
|
@ -131,7 +169,7 @@ func (ch *SendableChannel) completeChannels(word string) (entries []cchat.Comple
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := ch.session.State.Channels(ch.guildID)
|
c, err := ch.state.State.Channels(ch.guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -145,7 +183,7 @@ func (ch *SendableChannel) completeChannels(word string) (entries []cchat.Comple
|
||||||
|
|
||||||
var category string
|
var category string
|
||||||
if channel.CategoryID.IsValid() {
|
if channel.CategoryID.IsValid() {
|
||||||
if c, _ := ch.session.Store.Channel(channel.CategoryID); c != nil {
|
if c, _ := ch.state.Store.Channel(channel.CategoryID); c != nil {
|
||||||
category = c.Name
|
category = c.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,13 +202,13 @@ func (ch *SendableChannel) completeChannels(word string) (entries []cchat.Comple
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *SendableChannel) completeEmojis(word string) (entries []cchat.CompletionEntry) {
|
func (ch *Channel) completeEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||||
// Ignore if empty word.
|
// Ignore if empty word.
|
||||||
if word == "" {
|
if word == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
e, err := ch.session.EmojiState.Get(ch.guildID)
|
e, err := ch.state.EmojiState.Get(ch.guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cchat.Editor = (*Channel)(nil)
|
||||||
|
|
||||||
|
// IsEditor returns true if the user can send messages in this channel.
|
||||||
|
func (ch *Channel) IsEditor() bool {
|
||||||
|
p, err := ch.state.StateOnly().Permissions(ch.id, ch.state.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Has(discord.PermissionSendMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageEditable returns true if the given message ID belongs to the current
|
||||||
|
// user.
|
||||||
|
func (ch *Channel) MessageEditable(id string) bool {
|
||||||
|
s, err := discord.ParseSnowflake(id)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ch.state.Store.Message(ch.id, discord.MessageID(s))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Author.ID == ch.state.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawMessageContent returns the raw message content from Discord.
|
||||||
|
func (ch *Channel) RawMessageContent(id string) (string, error) {
|
||||||
|
s, err := discord.ParseSnowflake(id)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "Failed to parse ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ch.state.Store.Message(ch.id, discord.MessageID(s))
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "Failed to get the message")
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditMessage edits the message to the given content string.
|
||||||
|
func (ch *Channel) EditMessage(id, content string) error {
|
||||||
|
s, err := discord.ParseSnowflake(id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to parse ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ch.state.EditText(ch.id, discord.MessageID(s), content)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/typer"
|
||||||
|
"github.com/diamondburned/ningen/states/read"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.TypingIndicator = (*Channel)(nil)
|
||||||
|
_ cchat.UnreadIndicator = (*Channel)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsTypingIndicator returns true.
|
||||||
|
func (ch *Channel) IsTypingIndicator() bool { return true }
|
||||||
|
|
||||||
|
func (ch *Channel) Typing() error {
|
||||||
|
return ch.state.Typing(ch.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypingTimeout returns 10 seconds.
|
||||||
|
func (ch *Channel) TypingTimeout() time.Duration {
|
||||||
|
return 10 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) TypingSubscribe(ti cchat.TypingContainer) (func(), error) {
|
||||||
|
return ch.state.AddHandler(func(t *gateway.TypingStartEvent) {
|
||||||
|
// Ignore channel mismatch or if the typing event is ours.
|
||||||
|
if t.ChannelID != ch.id || t.UserID == ch.state.UserID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if typer, err := typer.New(ch.state, t); err == nil {
|
||||||
|
ti.AddTyper(typer)
|
||||||
|
}
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// muted returns if this channel is muted. This includes the channel's category
|
||||||
|
// and guild.
|
||||||
|
func (ch *Channel) muted() bool {
|
||||||
|
return (ch.guildID.IsValid() && ch.state.MutedState.Guild(ch.guildID, false)) ||
|
||||||
|
ch.state.MutedState.Channel(ch.id) ||
|
||||||
|
ch.state.MutedState.Category(ch.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUnreadIndicator returns true.
|
||||||
|
func (ch *Channel) IsUnreadIndicator() bool { return true }
|
||||||
|
|
||||||
|
func (ch *Channel) UnreadIndicate(indicator cchat.UnreadContainer) (func(), error) {
|
||||||
|
if rs := ch.state.ReadState.FindLast(ch.id); rs != nil {
|
||||||
|
c, err := ch.self()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Failed to get self channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LastMessageID > rs.LastMessageID && !ch.muted() {
|
||||||
|
indicator.SetUnread(true, rs.MentionCount > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ch.state.ReadState.OnUpdate(func(ev *read.UpdateEvent) {
|
||||||
|
if ch.id == ev.ChannelID && !ch.muted() {
|
||||||
|
indicator.SetUnread(ev.Unread, ev.MentionCount > 0)
|
||||||
|
}
|
||||||
|
}), nil
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package memberlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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/segments"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Member struct {
|
||||||
|
Channel
|
||||||
|
state *state.Instance
|
||||||
|
|
||||||
|
userID discord.UserID
|
||||||
|
origName string // use if cache is stale
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.ListMember = (*Member)(nil)
|
||||||
|
_ cchat.Iconer = (*Member)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// New creates a new list member. it.Member must not be nil.
|
||||||
|
func (c Channel) NewMember(opItem gateway.GuildMemberListOpItem) *Member {
|
||||||
|
return &Member{
|
||||||
|
Channel: c,
|
||||||
|
userID: opItem.Member.User.ID,
|
||||||
|
origName: opItem.Member.User.Username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Member) ID() cchat.ID {
|
||||||
|
return l.userID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Member) Name() text.Rich {
|
||||||
|
g, err := l.state.Store.Guild(l.guildID)
|
||||||
|
if err != nil {
|
||||||
|
return text.Plain(l.origName)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := l.state.Store.Member(l.guildID, l.userID)
|
||||||
|
if err != nil {
|
||||||
|
return text.Plain(l.origName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = m.User.Username
|
||||||
|
if m.Nick != "" {
|
||||||
|
name = m.Nick
|
||||||
|
}
|
||||||
|
|
||||||
|
mention := segments.MemberSegment(0, len(name), *g, *m)
|
||||||
|
mention.WithState(l.state.State)
|
||||||
|
|
||||||
|
var txt = text.Rich{
|
||||||
|
Content: name,
|
||||||
|
Segments: []text.Segment{mention},
|
||||||
|
}
|
||||||
|
|
||||||
|
if c := discord.MemberColor(*g, *m); c != discord.DefaultMemberColor {
|
||||||
|
txt.Segments = append(txt.Segments, segments.NewColored(len(name), uint32(c)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return txt
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIconer returns true.
|
||||||
|
func (l *Member) IsIconer() bool { return true }
|
||||||
|
|
||||||
|
func (l *Member) Icon(ctx context.Context, c cchat.IconContainer) (func(), error) {
|
||||||
|
m, err := l.state.Member(l.guildID, l.userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetIcon(urlutils.AvatarURL(m.User.AvatarURL()))
|
||||||
|
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Member) Status() cchat.UserStatus {
|
||||||
|
p, err := l.state.Store.Presence(l.guildID, l.userID)
|
||||||
|
if err != nil {
|
||||||
|
return cchat.UnknownStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.Status {
|
||||||
|
case discord.OnlineStatus:
|
||||||
|
return cchat.OnlineStatus
|
||||||
|
case discord.DoNotDisturbStatus:
|
||||||
|
return cchat.BusyStatus
|
||||||
|
case discord.IdleStatus:
|
||||||
|
return cchat.AwayStatus
|
||||||
|
case discord.OfflineStatus, discord.InvisibleStatus:
|
||||||
|
return cchat.OfflineStatus
|
||||||
|
default:
|
||||||
|
return cchat.UnknownStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Member) Secondary() text.Rich {
|
||||||
|
p, err := l.state.Store.Presence(l.guildID, l.userID)
|
||||||
|
if err != nil {
|
||||||
|
return text.Plain("")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Game != nil {
|
||||||
|
return formatSmallActivity(*p.Game)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.Activities) > 0 {
|
||||||
|
return formatSmallActivity(p.Activities[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.Plain("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSmallActivity(ac discord.Activity) text.Rich {
|
||||||
|
switch ac.Type {
|
||||||
|
case discord.GameActivity:
|
||||||
|
return text.Plain(fmt.Sprintf("Playing %s", ac.Name))
|
||||||
|
|
||||||
|
case discord.ListeningActivity:
|
||||||
|
return text.Plain(fmt.Sprintf("Listening to %s", ac.Name))
|
||||||
|
|
||||||
|
case discord.StreamingActivity:
|
||||||
|
return text.Plain(fmt.Sprintf("Streaming on %s", ac.Name))
|
||||||
|
|
||||||
|
case discord.CustomActivity:
|
||||||
|
var status strings.Builder
|
||||||
|
var segmts []text.Segment
|
||||||
|
|
||||||
|
if ac.Emoji != nil {
|
||||||
|
if !ac.Emoji.ID.IsValid() {
|
||||||
|
status.WriteString(ac.Emoji.Name)
|
||||||
|
status.WriteByte(' ')
|
||||||
|
} else {
|
||||||
|
segmts = append(segmts, segments.EmojiSegment{
|
||||||
|
Start: status.Len(),
|
||||||
|
Name: ac.Emoji.Name,
|
||||||
|
EmojiURL: ac.Emoji.EmojiURL() + "?size=64",
|
||||||
|
Large: ac.State == "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status.WriteString(ac.State)
|
||||||
|
|
||||||
|
return text.Rich{
|
||||||
|
Content: status.String(),
|
||||||
|
Segments: segmts,
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return text.Rich{}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package memberlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/ningen/states/member"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
// Keep stateful references to do on-demand loading.
|
||||||
|
state *state.Instance
|
||||||
|
// constant states
|
||||||
|
channelID discord.ChannelID
|
||||||
|
guildID discord.GuildID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChannel(s *state.Instance, ch discord.ChannelID, g discord.GuildID) Channel {
|
||||||
|
return Channel{
|
||||||
|
state: s,
|
||||||
|
channelID: ch,
|
||||||
|
guildID: g,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch Channel) FlushMemberGroups(l *member.List, c cchat.MemberListContainer) {
|
||||||
|
l.ViewGroups(func(groups []gateway.GuildMemberListGroup) {
|
||||||
|
var sections = make([]cchat.MemberSection, len(groups))
|
||||||
|
for i, group := range groups {
|
||||||
|
sections[i] = ch.NewSection(l.ID(), group)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetSections(sections)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package memberlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Section struct {
|
||||||
|
Channel
|
||||||
|
|
||||||
|
// constant states
|
||||||
|
listID string
|
||||||
|
id string // roleID or online or offline
|
||||||
|
name string
|
||||||
|
total int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.MemberSection = (*Section)(nil)
|
||||||
|
_ cchat.MemberDynamicSection = (*Section)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ch Channel) NewSection(listID string, group gateway.GuildMemberListGroup) *Section {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
switch group.ID {
|
||||||
|
case "online":
|
||||||
|
name = "Online"
|
||||||
|
case "offline":
|
||||||
|
name = "Offline"
|
||||||
|
default:
|
||||||
|
p, err := discord.ParseSnowflake(group.ID)
|
||||||
|
if err != nil {
|
||||||
|
name = group.ID
|
||||||
|
} else {
|
||||||
|
r, err := ch.state.Role(ch.guildID, discord.RoleID(p))
|
||||||
|
if err != nil {
|
||||||
|
name = fmt.Sprintf("<@#%s>", p.String())
|
||||||
|
} else {
|
||||||
|
name = r.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Section{
|
||||||
|
Channel: ch,
|
||||||
|
listID: listID,
|
||||||
|
id: group.ID,
|
||||||
|
name: name,
|
||||||
|
total: int(group.Count),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Section) ID() cchat.ID {
|
||||||
|
return s.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Section) Name() text.Rich {
|
||||||
|
return text.Rich{Content: s.name}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Section) Total() int {
|
||||||
|
return s.total
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Section) IsMemberDynamicSection() bool { return true }
|
||||||
|
|
||||||
|
// TODO: document that Load{More,Less} works more like a shifting window.
|
||||||
|
|
||||||
|
func (s *Section) LoadMore() bool {
|
||||||
|
chunk := s.state.MemberState.GetMemberListChunk(s.guildID, s.channelID)
|
||||||
|
if chunk < 0 {
|
||||||
|
chunk = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.state.MemberState.RequestMemberList(s.guildID, s.channelID, chunk) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Section) LoadLess() bool {
|
||||||
|
chunk := s.state.MemberState.GetMemberListChunk(s.guildID, s.channelID)
|
||||||
|
if chunk <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.state.MemberState.RequestMemberList(s.guildID, s.channelID, chunk-1)
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/memberlist"
|
||||||
|
"github.com/diamondburned/ningen/states/member"
|
||||||
|
)
|
||||||
|
|
||||||
|
func seekPrevGroup(l *member.List, ix int) (item, group gateway.GuildMemberListOpItem) {
|
||||||
|
l.ViewItems(func(items []gateway.GuildMemberListOpItem) {
|
||||||
|
// Bound check.
|
||||||
|
if ix >= len(items) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item = items[ix]
|
||||||
|
|
||||||
|
// Search backwards.
|
||||||
|
for i := ix; i >= 0; i-- {
|
||||||
|
if items[i].Group != nil {
|
||||||
|
group = items[i]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.MemberLister = (*Channel)(nil)
|
||||||
|
|
||||||
|
// IsMemberLister returns true if the channel is a guild channel.
|
||||||
|
func (ch *Channel) IsMemberLister() bool {
|
||||||
|
return ch.guildID.IsValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) memberListCh() memberlist.Channel {
|
||||||
|
return memberlist.NewChannel(ch.state, ch.id, ch.guildID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) ListMembers(ctx context.Context, c cchat.MemberListContainer) (func(), error) {
|
||||||
|
if !ch.guildID.IsValid() {
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel := ch.state.AddHandler(func(u *gateway.GuildMemberListUpdate) {
|
||||||
|
l, err := ch.state.MemberState.GetMemberList(ch.guildID, ch.id)
|
||||||
|
if err != nil {
|
||||||
|
return // wat
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.GuildID() != u.GuildID || l.ID() != u.ID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listCh = ch.memberListCh()
|
||||||
|
|
||||||
|
for _, ev := range u.Ops {
|
||||||
|
switch ev.Op {
|
||||||
|
case "SYNC":
|
||||||
|
ch.checkSync(c)
|
||||||
|
|
||||||
|
case "INSERT", "UPDATE":
|
||||||
|
item, group := seekPrevGroup(l, ev.Index)
|
||||||
|
if item.Member != nil && group.Group != nil {
|
||||||
|
c.SetMember(group.Group.ID, listCh.NewMember(item))
|
||||||
|
listCh.FlushMemberGroups(l, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DELETE":
|
||||||
|
_, group := seekPrevGroup(l, ev.Index-1)
|
||||||
|
if group.Group != nil && ev.Item.Member != nil {
|
||||||
|
c.RemoveMember(group.Group.ID, ev.Item.Member.User.ID.String())
|
||||||
|
listCh.FlushMemberGroups(l, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ch.checkSync(c)
|
||||||
|
|
||||||
|
return cancel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) checkSync(c cchat.MemberListContainer) {
|
||||||
|
l, err := ch.state.MemberState.GetMemberList(ch.guildID, ch.id)
|
||||||
|
if err != nil {
|
||||||
|
ch.state.MemberState.RequestMemberList(ch.guildID, ch.id, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listCh := ch.memberListCh()
|
||||||
|
listCh.FlushMemberGroups(l, c)
|
||||||
|
|
||||||
|
l.ViewItems(func(items []gateway.GuildMemberListOpItem) {
|
||||||
|
var group gateway.GuildMemberListGroup
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
switch {
|
||||||
|
case item.Group != nil:
|
||||||
|
group = *item.Group
|
||||||
|
|
||||||
|
case item.Member != nil:
|
||||||
|
c.SetMember(group.ID, listCh.NewMember(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"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/funcutil"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cchat.Messenger = (*Channel)(nil)
|
||||||
|
|
||||||
|
// IsMessenger returns true if the current user is allowed to see the channel.
|
||||||
|
func (ch *Channel) IsMessenger() bool {
|
||||||
|
p, err := ch.state.StateOnly().Permissions(ch.id, ch.state.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Has(discord.PermissionViewChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
|
||||||
|
state := ch.state.WithContext(ctx)
|
||||||
|
|
||||||
|
m, err := state.Messages(ch.id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var addcancel = funcutil.NewCancels()
|
||||||
|
|
||||||
|
var constructor func(discord.Message) cchat.MessageCreate
|
||||||
|
|
||||||
|
if ch.guildID.IsValid() {
|
||||||
|
// Create the backlog without any member information.
|
||||||
|
g, err := state.Guild(ch.guildID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Failed to get guild")
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor = func(m discord.Message) cchat.MessageCreate {
|
||||||
|
return message.NewBacklogMessage(m, ch.state, *g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to typing events.
|
||||||
|
ch.state.MemberState.Subscribe(ch.guildID)
|
||||||
|
|
||||||
|
// Listen to new members before creating the backlog and requesting members.
|
||||||
|
addcancel(ch.state.AddHandler(func(c *gateway.GuildMembersChunkEvent) {
|
||||||
|
if c.GuildID != ch.guildID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ch.messages()
|
||||||
|
if err != nil {
|
||||||
|
// TODO: log
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g, err := ch.guild()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over all messages and replace the author. The latest
|
||||||
|
// messages are in front.
|
||||||
|
for _, msg := range m {
|
||||||
|
for _, member := range c.Members {
|
||||||
|
if msg.Author.ID != member.User.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.UpdateMessage(message.NewMessageUpdateAuthor(msg, member, *g, ch.state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
constructor = func(m discord.Message) cchat.MessageCreate {
|
||||||
|
return message.NewDirectMessage(m, ch.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only do all this if we even have any messages.
|
||||||
|
if len(m) > 0 {
|
||||||
|
// Sort messages chronologically using the ID so that the oldest messages
|
||||||
|
// (ones with the smallest snowflake) is in front.
|
||||||
|
sort.Slice(m, func(i, j int) bool { return m[i].ID < m[j].ID })
|
||||||
|
|
||||||
|
// Iterate from the earliest messages to the latest messages.
|
||||||
|
for _, m := range m {
|
||||||
|
ct.CreateMessage(constructor(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark this channel as read.
|
||||||
|
ch.state.ReadState.MarkRead(ch.id, m[len(m)-1].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind the handler.
|
||||||
|
addcancel(
|
||||||
|
ch.state.AddHandler(func(m *gateway.MessageCreateEvent) {
|
||||||
|
if m.ChannelID == ch.id {
|
||||||
|
ct.CreateMessage(message.NewMessageCreate(m, ch.state))
|
||||||
|
ch.state.ReadState.MarkRead(ch.id, m.ID)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ch.state.AddHandler(func(m *gateway.MessageUpdateEvent) {
|
||||||
|
// If the updated content is empty. TODO: add embed support.
|
||||||
|
if m.ChannelID == ch.id {
|
||||||
|
ct.UpdateMessage(message.NewMessageUpdateContent(m.Message, ch.state))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ch.state.AddHandler(func(m *gateway.MessageDeleteEvent) {
|
||||||
|
if m.ChannelID == ch.id {
|
||||||
|
ct.DeleteMessage(message.NewHeaderDelete(m))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return funcutil.JoinCancels(addcancel()), nil
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/segments"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cchat.Nicknamer = (*Channel)(nil)
|
||||||
|
|
||||||
|
// IsNicknamer returns true if the current channel is in a guild.
|
||||||
|
func (ch *Channel) IsNicknamer() bool {
|
||||||
|
return ch.guildID.IsValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) (func(), error) {
|
||||||
|
// We don't have a nickname if we're not in a guild.
|
||||||
|
if !ch.guildID.IsValid() {
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state := ch.state.WithContext(ctx)
|
||||||
|
|
||||||
|
// MemberColor should fill up the state cache.
|
||||||
|
c, err := state.MemberColor(ch.guildID, ch.state.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Failed to get self member color")
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := state.Member(ch.guildID, ch.state.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Failed to get self member")
|
||||||
|
}
|
||||||
|
|
||||||
|
var rich = text.Rich{Content: m.User.Username}
|
||||||
|
if m.Nick != "" {
|
||||||
|
rich.Content = m.Nick
|
||||||
|
}
|
||||||
|
if c > 0 {
|
||||||
|
rich.Segments = []text.Segment{
|
||||||
|
segments.NewColored(len(rich.Content), c.Uint32()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labeler.SetLabel(rich)
|
||||||
|
|
||||||
|
// Copy the user ID to use.
|
||||||
|
var selfID = m.User.ID
|
||||||
|
|
||||||
|
return ch.state.AddHandler(func(g *gateway.GuildMemberUpdateEvent) {
|
||||||
|
if g.GuildID != ch.guildID || g.User.ID != selfID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rich = text.Rich{Content: m.User.Username}
|
||||||
|
if m.Nick != "" {
|
||||||
|
rich.Content = m.Nick
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := ch.state.MemberColor(g.GuildID, selfID)
|
||||||
|
if err == nil {
|
||||||
|
rich.Segments = []text.Segment{
|
||||||
|
segments.NewColored(len(rich.Content), c.Uint32()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labeler.SetLabel(rich)
|
||||||
|
}), nil
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/arikawa/api"
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.MessageSender = (*Channel)(nil)
|
||||||
|
_ cchat.AttachmentSender = (*Channel)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ch *Channel) IsMessageSender() bool {
|
||||||
|
p, err := ch.state.StateOnly().Permissions(ch.id, ch.state.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Has(discord.PermissionSendMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) SendMessage(msg cchat.SendableMessage) error {
|
||||||
|
var send = api.SendMessageData{Content: msg.Content()}
|
||||||
|
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
||||||
|
send.Nonce = noncer.Nonce()
|
||||||
|
}
|
||||||
|
if attcher, ok := msg.(cchat.Attachments); ok {
|
||||||
|
send.Files = addAttachments(attcher.Attachments())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ch.state.SendMessageComplex(ch.id, send)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAttachmentSender returns true if the channel can attach files.
|
||||||
|
func (ch *Channel) IsAttachmentSender() bool {
|
||||||
|
p, err := ch.state.StateOnly().Permissions(ch.id, ch.state.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Has(discord.PermissionAttachFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *Channel) SendAttachments(atts []cchat.MessageAttachment) error {
|
||||||
|
_, err := ch.state.SendMessageComplex(ch.id, api.SendMessageData{
|
||||||
|
Files: addAttachments(atts),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAttachments(atts []cchat.MessageAttachment) []api.SendMessageFile {
|
||||||
|
var files = make([]api.SendMessageFile, len(atts))
|
||||||
|
for i, a := range atts {
|
||||||
|
files[i] = api.SendMessageFile{
|
||||||
|
Name: a.Name,
|
||||||
|
Reader: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
|
@ -1,29 +1,31 @@
|
||||||
package discord
|
package typer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/pkg/errors"
|
"github.com/diamondburned/cchat-discord/internal/discord/message"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Typer struct {
|
type Typer struct {
|
||||||
Author
|
message.Author
|
||||||
time discord.UnixTimestamp
|
time discord.UnixTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cchat.Typer = (*Typer)(nil)
|
var _ cchat.Typer = (*Typer)(nil)
|
||||||
|
|
||||||
func NewTyperAuthor(author Author, ev *gateway.TypingStartEvent) Typer {
|
func NewFromAuthor(author message.Author, ev *gateway.TypingStartEvent) Typer {
|
||||||
return Typer{
|
return Typer{
|
||||||
Author: author,
|
Author: author,
|
||||||
time: ev.Timestamp,
|
time: ev.Timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTyper(s *Session, ev *gateway.TypingStartEvent) (*Typer, error) {
|
func New(s *state.Instance, ev *gateway.TypingStartEvent) (*Typer, error) {
|
||||||
if ev.GuildID.IsValid() {
|
if ev.GuildID.IsValid() {
|
||||||
g, err := s.Store.Guild(ev.GuildID)
|
g, err := s.Store.Guild(ev.GuildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -38,7 +40,7 @@ func NewTyper(s *Session, ev *gateway.TypingStartEvent) (*Typer, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Typer{
|
return &Typer{
|
||||||
Author: NewGuildMember(*ev.Member, *g, s),
|
Author: message.NewGuildMember(*ev.Member, *g, s),
|
||||||
time: ev.Timestamp,
|
time: ev.Timestamp,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -51,7 +53,7 @@ func NewTyper(s *Session, ev *gateway.TypingStartEvent) (*Typer, error) {
|
||||||
for _, user := range c.DMRecipients {
|
for _, user := range c.DMRecipients {
|
||||||
if user.ID == ev.UserID {
|
if user.ID == ev.UserID {
|
||||||
return &Typer{
|
return &Typer{
|
||||||
Author: NewUser(user, s),
|
Author: message.NewUser(user, s),
|
||||||
time: ev.Timestamp,
|
time: ev.Timestamp,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package folder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/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"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GuildFolder struct {
|
||||||
|
gateway.GuildFolder
|
||||||
|
state *state.Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.Server = (*GuildFolder)(nil)
|
||||||
|
_ cchat.Lister = (*GuildFolder)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(s *state.Instance, gf gateway.GuildFolder) *GuildFolder {
|
||||||
|
// Name should never be empty.
|
||||||
|
if gf.Name == "" {
|
||||||
|
var names = make([]string, 0, len(gf.GuildIDs))
|
||||||
|
|
||||||
|
for _, id := range gf.GuildIDs {
|
||||||
|
if g, _ := s.Store.Guild(id); g != 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.
|
||||||
|
segments.NewColored(len(name.Content), gf.GuildFolder.Color.Uint32()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLister returns true.
|
||||||
|
func (gf *GuildFolder) IsLister() bool { return true }
|
||||||
|
|
||||||
|
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.Guild(id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
servers = append(servers, guild.New(gf.state, g))
|
||||||
|
}
|
||||||
|
|
||||||
|
container.SetServers(servers)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package guild
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/arikawa/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/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Guild struct {
|
||||||
|
id discord.GuildID
|
||||||
|
state *state.Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.Iconer = (*Guild)(nil)
|
||||||
|
_ cchat.Server = (*Guild)(nil)
|
||||||
|
_ cchat.Lister = (*Guild)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(s *state.Instance, g *discord.Guild) *Guild {
|
||||||
|
return &Guild{
|
||||||
|
id: g.ID,
|
||||||
|
state: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromID(s *state.Instance, gID discord.GuildID) (*Guild, error) {
|
||||||
|
g, err := s.Guild(gID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(s, g), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Guild) self(ctx context.Context) (*discord.Guild, error) {
|
||||||
|
return g.state.WithContext(ctx).Guild(g.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Guild) selfState() (*discord.Guild, error) {
|
||||||
|
return g.state.Store.Guild(g.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Guild) ID() cchat.ID {
|
||||||
|
return g.id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Guild) Name() text.Rich {
|
||||||
|
s, err := g.selfState()
|
||||||
|
if err != nil {
|
||||||
|
// This shouldn't happen.
|
||||||
|
return text.Rich{Content: g.id.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.Rich{Content: s.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIconer returns true if the guild has an icon.
|
||||||
|
func (g *Guild) IsIconer() bool {
|
||||||
|
s, err := g.selfState()
|
||||||
|
return err == nil && s.Icon != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||||
|
s, err := g.self(ctx)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLister returns true.
|
||||||
|
func (g *Guild) IsLister() bool { return true }
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
var chs = make([]cchat.Server, 0, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.SetServers(chs)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/segments"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
id discord.UserID
|
||||||
|
name text.Rich
|
||||||
|
avatar string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUser(u discord.User, s *state.Instance) Author {
|
||||||
|
var name = text.Rich{Content: u.Username}
|
||||||
|
if u.Bot {
|
||||||
|
name.Content += " "
|
||||||
|
name.Segments = append(name.Segments,
|
||||||
|
segments.NewBlurpleSegment(segments.Write(&name, "[BOT]")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a clickable user popup.
|
||||||
|
useg := segments.UserSegment(0, len(name.Content), u)
|
||||||
|
useg.WithState(s.State)
|
||||||
|
name.Segments = append(name.Segments, useg)
|
||||||
|
|
||||||
|
return Author{
|
||||||
|
id: u.ID,
|
||||||
|
name: name,
|
||||||
|
avatar: urlutils.AvatarURL(u.AvatarURL()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGuildMember(m discord.Member, g discord.Guild, s *state.Instance) Author {
|
||||||
|
return Author{
|
||||||
|
id: m.User.ID,
|
||||||
|
name: RenderMemberName(m, g, s),
|
||||||
|
avatar: urlutils.AvatarURL(m.User.AvatarURL()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderMemberName(m discord.Member, g discord.Guild, s *state.Instance) text.Rich {
|
||||||
|
var name = text.Rich{
|
||||||
|
Content: m.User.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the nickname.
|
||||||
|
if m.Nick != "" {
|
||||||
|
name.Content = m.Nick
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the color.
|
||||||
|
if c := discord.MemberColor(g, m); c > 0 {
|
||||||
|
name.Segments = append(name.Segments,
|
||||||
|
segments.NewColored(len(name.Content), c.Uint32()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the bot prefix if the user is a bot.
|
||||||
|
if m.User.Bot {
|
||||||
|
name.Content += " "
|
||||||
|
name.Segments = append(name.Segments,
|
||||||
|
segments.NewBlurpleSegment(segments.Write(&name, "[BOT]")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a clickable user popup.
|
||||||
|
useg := segments.MemberSegment(0, len(name.Content), g, m)
|
||||||
|
useg.WithState(s.State)
|
||||||
|
name.Segments = append(name.Segments, useg)
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Author) ID() cchat.ID {
|
||||||
|
return a.id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Author) Name() text.Rich {
|
||||||
|
return a.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Author) Avatar() string {
|
||||||
|
return a.avatar
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package discord
|
package message
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
@ -6,8 +6,8 @@ import (
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/segments"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat-discord/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/segments"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,91 +52,6 @@ func (m messageHeader) Time() time.Time {
|
||||||
return m.time.Time()
|
return m.time.Time()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AvatarURL wraps the URL with URL queries for the avatar.
|
|
||||||
func AvatarURL(URL string) string {
|
|
||||||
return urlutils.AvatarURL(URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Author struct {
|
|
||||||
id discord.UserID
|
|
||||||
name text.Rich
|
|
||||||
avatar string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUser(u discord.User, s *Session) Author {
|
|
||||||
var name = text.Rich{Content: u.Username}
|
|
||||||
if u.Bot {
|
|
||||||
name.Content += " "
|
|
||||||
name.Segments = append(name.Segments,
|
|
||||||
segments.NewBlurpleSegment(segments.Write(&name, "[BOT]")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append a clickable user popup.
|
|
||||||
useg := segments.UserSegment(0, len(name.Content), u)
|
|
||||||
useg.WithState(s.State)
|
|
||||||
name.Segments = append(name.Segments, useg)
|
|
||||||
|
|
||||||
return Author{
|
|
||||||
id: u.ID,
|
|
||||||
name: name,
|
|
||||||
avatar: AvatarURL(u.AvatarURL()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGuildMember(m discord.Member, g discord.Guild, s *Session) Author {
|
|
||||||
return Author{
|
|
||||||
id: m.User.ID,
|
|
||||||
name: RenderMemberName(m, g, s),
|
|
||||||
avatar: AvatarURL(m.User.AvatarURL()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderMemberName(m discord.Member, g discord.Guild, s *Session) text.Rich {
|
|
||||||
var name = text.Rich{
|
|
||||||
Content: m.User.Username,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the nickname.
|
|
||||||
if m.Nick != "" {
|
|
||||||
name.Content = m.Nick
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the color.
|
|
||||||
if c := discord.MemberColor(g, m); c > 0 {
|
|
||||||
name.Segments = append(name.Segments,
|
|
||||||
segments.NewColored(len(name.Content), c.Uint32()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the bot prefix if the user is a bot.
|
|
||||||
if m.User.Bot {
|
|
||||||
name.Content += " "
|
|
||||||
name.Segments = append(name.Segments,
|
|
||||||
segments.NewBlurpleSegment(segments.Write(&name, "[BOT]")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append a clickable user popup.
|
|
||||||
useg := segments.MemberSegment(0, len(name.Content), g, m)
|
|
||||||
useg.WithState(s.State)
|
|
||||||
name.Segments = append(name.Segments, useg)
|
|
||||||
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Author) ID() cchat.ID {
|
|
||||||
return a.id.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Author) Name() text.Rich {
|
|
||||||
return a.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Author) Avatar() string {
|
|
||||||
return a.avatar
|
|
||||||
}
|
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
messageHeader
|
messageHeader
|
||||||
|
|
||||||
|
@ -147,7 +62,7 @@ type Message struct {
|
||||||
mentioned bool
|
mentioned bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessageUpdateContent(msg discord.Message, s *Session) Message {
|
func NewMessageUpdateContent(msg discord.Message, s *state.Instance) Message {
|
||||||
// Check if content is empty.
|
// Check if content is empty.
|
||||||
if msg.Content == "" {
|
if msg.Content == "" {
|
||||||
// Then grab the content from the state.
|
// Then grab the content from the state.
|
||||||
|
@ -164,7 +79,7 @@ func NewMessageUpdateContent(msg discord.Message, s *Session) Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessageUpdateAuthor(
|
func NewMessageUpdateAuthor(
|
||||||
msg discord.Message, member discord.Member, g discord.Guild, s *Session) Message {
|
msg discord.Message, member discord.Member, g discord.Guild, s *state.Instance) Message {
|
||||||
|
|
||||||
return Message{
|
return Message{
|
||||||
messageHeader: newHeader(msg),
|
messageHeader: newHeader(msg),
|
||||||
|
@ -174,7 +89,7 @@ func NewMessageUpdateAuthor(
|
||||||
|
|
||||||
// NewMessageCreate uses the session to create a message. It does not do
|
// NewMessageCreate uses the session to create a message. It does not do
|
||||||
// API calls. Member is optional.
|
// API calls. Member is optional.
|
||||||
func NewMessageCreate(c *gateway.MessageCreateEvent, s *Session) Message {
|
func NewMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Message {
|
||||||
// This should not error.
|
// This should not error.
|
||||||
g, err := s.Store.Guild(c.GuildID)
|
g, err := s.Store.Guild(c.GuildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -195,7 +110,7 @@ func NewMessageCreate(c *gateway.MessageCreateEvent, s *Session) Message {
|
||||||
// NewBacklogMessage uses the session to create a message fetched from the
|
// NewBacklogMessage uses the session to create a message fetched from the
|
||||||
// backlog. It takes in an existing guild and tries to fetch a new member, if
|
// backlog. It takes in an existing guild and tries to fetch a new member, if
|
||||||
// it's nil.
|
// it's nil.
|
||||||
func NewBacklogMessage(m discord.Message, s *Session, g discord.Guild) Message {
|
func NewBacklogMessage(m discord.Message, s *state.Instance, g discord.Guild) Message {
|
||||||
// If the message doesn't have a guild, then we don't need all the
|
// If the message doesn't have a guild, then we don't need all the
|
||||||
// complicated member fetching process.
|
// complicated member fetching process.
|
||||||
if !m.GuildID.IsValid() {
|
if !m.GuildID.IsValid() {
|
||||||
|
@ -211,11 +126,11 @@ func NewBacklogMessage(m discord.Message, s *Session, g discord.Guild) Message {
|
||||||
return NewMessage(m, s, NewGuildMember(*mem, g, s))
|
return NewMessage(m, s, NewGuildMember(*mem, g, s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDirectMessage(m discord.Message, s *Session) Message {
|
func NewDirectMessage(m discord.Message, s *state.Instance) Message {
|
||||||
return NewMessage(m, s, NewUser(m.Author, s))
|
return NewMessage(m, s, NewUser(m.Author, s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessage(m discord.Message, s *Session, author Author) Message {
|
func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
|
||||||
// Render the message content.
|
// Render the message content.
|
||||||
var content = segments.ParseMessage(&m, s.Store)
|
var content = segments.ParseMessage(&m, s.Store)
|
||||||
|
|
||||||
|
@ -248,7 +163,7 @@ func NewMessage(m discord.Message, s *Session, author Author) Message {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Message) Author() cchat.MessageAuthor {
|
func (m Message) Author() cchat.Author {
|
||||||
if !m.author.id.IsValid() {
|
if !m.author.id.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/arikawa/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/state"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/ningen"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
*state.Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.Iconer = (*Session)(nil)
|
||||||
|
_ cchat.Session = (*Session)(nil)
|
||||||
|
_ cchat.SessionSaver = (*Session)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFromToken(token string) (*Session, error) {
|
||||||
|
i, err := state.NewFromToken(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Session{i}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) ID() cchat.ID {
|
||||||
|
return s.UserID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Name() text.Rich {
|
||||||
|
u, err := s.Store.Me()
|
||||||
|
if err != nil {
|
||||||
|
// This shouldn't happen, ever.
|
||||||
|
return text.Rich{Content: "<@" + s.UserID.String() + ">"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.Rich{Content: u.Username + "#" + u.Discriminator}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIconer returns true.
|
||||||
|
func (s *Session) IsIconer() bool { return true }
|
||||||
|
|
||||||
|
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||||
|
u, err := s.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.AddHandler(func(*gateway.UserUpdateEvent) {
|
||||||
|
// Bypass the event and use the state cache.
|
||||||
|
if u, err := s.Store.Me(); err == nil {
|
||||||
|
iconer.SetIcon(urlutils.AvatarURL(u.AvatarURL()))
|
||||||
|
}
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Disconnect() error {
|
||||||
|
return s.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSessionSaver returns true.
|
||||||
|
func (s *Session) IsSessionSaver() bool { return true }
|
||||||
|
|
||||||
|
func (s *Session) SaveSession() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"token": s.Token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Servers(container cchat.ServersContainer) error {
|
||||||
|
// Reset the entire container when the session is closed.
|
||||||
|
s.AddHandler(func(*session.Closed) {
|
||||||
|
container.SetServers(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set the entire container again once reconnected.
|
||||||
|
s.AddHandler(func(*ningen.Connected) {
|
||||||
|
s.servers(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
return s.servers(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) servers(container cchat.ServersContainer) error {
|
||||||
|
switch {
|
||||||
|
// If the user has guild folders:
|
||||||
|
case len(s.Ready.Settings.GuildFolders) > 0:
|
||||||
|
// TODO: account for missing guilds.
|
||||||
|
var toplevels = make([]cchat.Server, 0, len(s.Ready.Settings.GuildFolders))
|
||||||
|
|
||||||
|
for _, guildFolder := range s.Ready.Settings.GuildFolders {
|
||||||
|
// TODO: correct.
|
||||||
|
switch {
|
||||||
|
case guildFolder.ID > 0:
|
||||||
|
fallthrough
|
||||||
|
case len(guildFolder.GuildIDs) > 1:
|
||||||
|
toplevels = append(toplevels, folder.New(s.Instance, guildFolder))
|
||||||
|
|
||||||
|
case len(guildFolder.GuildIDs) == 1:
|
||||||
|
g, err := guild.NewFromID(s.Instance, guildFolder.GuildIDs[0])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
toplevels = append(toplevels, g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.SetServers(toplevels)
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
for _, id := range s.Ready.Settings.GuildPositions {
|
||||||
|
g, err := guild.NewFromID(s.Instance, id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
guilds = append(guilds, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
container.SetServers(guilds)
|
||||||
|
|
||||||
|
// None of the above:
|
||||||
|
default:
|
||||||
|
g, err := s.Guilds()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var servers = make([]cchat.Server, len(g))
|
||||||
|
for i := range g {
|
||||||
|
servers[i] = guild.New(s.Instance, &g[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
container.SetServers(servers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Package state provides a shared state instance for other packages to use.
|
||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/arikawa/state"
|
||||||
|
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
|
||||||
|
"github.com/diamondburned/ningen"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Instance struct {
|
||||||
|
*ningen.State
|
||||||
|
UserID discord.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromToken(token string) (*Instance, error) {
|
||||||
|
s, err := state.New(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(s *state.State) (*Instance, error) {
|
||||||
|
// Prefetch user.
|
||||||
|
u, err := s.Me()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Failed to get current user")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Instance{
|
||||||
|
UserID: u.ID,
|
||||||
|
State: n,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateOnly returns a shallow copy of *State with an already-expired context.
|
||||||
|
func (s *Instance) StateOnly() *state.State {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
return s.WithContext(ctx)
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package funcutil
|
||||||
|
|
||||||
|
// NweCancels creates a stateful closure for adding callbacks into a list.
|
||||||
|
func NewCancels() func(...func()) []func() {
|
||||||
|
var cancels []func()
|
||||||
|
return func(appended ...func()) []func() {
|
||||||
|
cancels = append(cancels, appended...)
|
||||||
|
return cancels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinCancels joins multiple cancel callbacks into one.
|
||||||
|
func JoinCancels(cancellers []func()) func() {
|
||||||
|
return func() {
|
||||||
|
for _, c := range cancellers {
|
||||||
|
c()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/arikawa/state"
|
"github.com/diamondburned/arikawa/state"
|
||||||
"github.com/diamondburned/cchat-discord/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/diamondburned/ningen/md"
|
"github.com/diamondburned/ningen/md"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 211 KiB |
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/arikawa/state"
|
"github.com/diamondburned/arikawa/state"
|
||||||
"github.com/diamondburned/cchat-discord/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/diamondburned/ningen"
|
"github.com/diamondburned/ningen"
|
||||||
"github.com/diamondburned/ningen/md"
|
"github.com/diamondburned/ningen/md"
|
|
@ -16,6 +16,10 @@ func AvatarURL(URL string) string {
|
||||||
|
|
||||||
// Sized wraps the URL with the size query.
|
// Sized wraps the URL with the size query.
|
||||||
func Sized(URL string, size int) string {
|
func Sized(URL string, size int) string {
|
||||||
|
if URL == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
u, err := url.Parse(URL)
|
u, err := url.Parse(URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return URL
|
return URL
|
233
service.go
233
service.go
|
@ -1,233 +0,0 @@
|
||||||
package discord
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
|
||||||
"github.com/diamondburned/arikawa/session"
|
|
||||||
"github.com/diamondburned/arikawa/state"
|
|
||||||
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
|
|
||||||
"github.com/diamondburned/cchat"
|
|
||||||
"github.com/diamondburned/cchat/services"
|
|
||||||
"github.com/diamondburned/cchat/text"
|
|
||||||
"github.com/diamondburned/ningen"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
services.RegisterService(&Service{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
|
||||||
var ErrInvalidSession = errors.New("invalid session")
|
|
||||||
|
|
||||||
type Service struct{}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Icon = (*Service)(nil)
|
|
||||||
_ cchat.Service = (*Service)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (Service) Name() text.Rich {
|
|
||||||
return text.Rich{Content: "Discord"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Service) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
|
||||||
iconer.SetIcon("https://raw.githubusercontent.com/" +
|
|
||||||
"diamondburned/cchat-discord/himearikawa/discord_logo.png")
|
|
||||||
return func() {}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Service) Authenticate() cchat.Authenticator {
|
|
||||||
return Authenticator{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Service) RestoreSession(data map[string]string) (cchat.Session, error) {
|
|
||||||
tk, ok := data["token"]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrInvalidSession
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewSessionToken(tk)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Authenticator struct{}
|
|
||||||
|
|
||||||
var _ cchat.Authenticator = (*Authenticator)(nil)
|
|
||||||
|
|
||||||
func (Authenticator) AuthenticateForm() []cchat.AuthenticateEntry {
|
|
||||||
// TODO: username, password and 2FA
|
|
||||||
return []cchat.AuthenticateEntry{
|
|
||||||
{
|
|
||||||
Name: "Token",
|
|
||||||
Secret: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Authenticator) Authenticate(form []string) (cchat.Session, error) {
|
|
||||||
return NewSessionToken(form[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
*ningen.State
|
|
||||||
userID discord.UserID
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Icon = (*Session)(nil)
|
|
||||||
_ cchat.Session = (*Session)(nil)
|
|
||||||
_ cchat.SessionSaver = (*Session)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewSessionToken(token string) (*Session, error) {
|
|
||||||
s, err := state.New(token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewSession(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSession(s *state.State) (*Session, error) {
|
|
||||||
// Prefetch user.
|
|
||||||
u, err := s.Me()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Failed to get current user")
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Session{
|
|
||||||
userID: u.ID,
|
|
||||||
State: n,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) ID() cchat.ID {
|
|
||||||
return s.userID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Name() text.Rich {
|
|
||||||
u, err := s.Store.Me()
|
|
||||||
if err != nil {
|
|
||||||
// This shouldn't happen, ever.
|
|
||||||
return text.Rich{Content: "<@" + s.userID.String() + ">"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.Rich{Content: u.Username + "#" + u.Discriminator}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
|
||||||
u, err := s.Me()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Failed to get the current user")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thanks to arikawa, AvatarURL is never empty.
|
|
||||||
iconer.SetIcon(AvatarURL(u.AvatarURL()))
|
|
||||||
|
|
||||||
return s.AddHandler(func(*gateway.UserUpdateEvent) {
|
|
||||||
// Bypass the event and use the state cache.
|
|
||||||
if u, err := s.Store.Me(); err == nil {
|
|
||||||
iconer.SetIcon(AvatarURL(u.AvatarURL()))
|
|
||||||
}
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Disconnect() error {
|
|
||||||
return s.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Save() (map[string]string, error) {
|
|
||||||
return map[string]string{
|
|
||||||
"token": s.Token,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) Servers(container cchat.ServersContainer) error {
|
|
||||||
// Reset the entire container when the session is closed.
|
|
||||||
s.AddHandler(func(*session.Closed) {
|
|
||||||
container.SetServers(nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set the entire container again once reconnected.
|
|
||||||
s.AddHandler(func(*ningen.Connected) {
|
|
||||||
s.servers(container)
|
|
||||||
})
|
|
||||||
|
|
||||||
return s.servers(container)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) servers(container cchat.ServersContainer) error {
|
|
||||||
switch {
|
|
||||||
// If the user has guild folders:
|
|
||||||
case len(s.Ready.Settings.GuildFolders) > 0:
|
|
||||||
// TODO: account for missing guilds.
|
|
||||||
var toplevels = make([]cchat.Server, 0, len(s.Ready.Settings.GuildFolders))
|
|
||||||
|
|
||||||
for _, folder := range s.Ready.Settings.GuildFolders {
|
|
||||||
// TODO: correct.
|
|
||||||
switch {
|
|
||||||
case folder.ID > 0:
|
|
||||||
fallthrough
|
|
||||||
case len(folder.GuildIDs) > 1:
|
|
||||||
toplevels = append(toplevels, NewGuildFolder(s, folder))
|
|
||||||
|
|
||||||
case len(folder.GuildIDs) == 1:
|
|
||||||
g, err := NewGuildFromID(s, folder.GuildIDs[0])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
toplevels = append(toplevels, g)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
container.SetServers(toplevels)
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
|
|
||||||
for _, id := range s.Ready.Settings.GuildPositions {
|
|
||||||
g, err := NewGuildFromID(s, id)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
guilds = append(guilds, g)
|
|
||||||
}
|
|
||||||
|
|
||||||
container.SetServers(guilds)
|
|
||||||
|
|
||||||
// None of the above:
|
|
||||||
default:
|
|
||||||
g, err := s.Guilds()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var servers = make([]cchat.Server, len(g))
|
|
||||||
for i := range g {
|
|
||||||
servers[i] = NewGuild(s, &g[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
container.SetServers(servers)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue