mirror of
https://github.com/diamondburned/cchat-discord.git
synced 2024-11-22 14:12:47 +00:00
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
|
||||
}
|
79
discord.go
Normal file
79
discord.go
Normal file
|
@ -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 (
|
||||
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/dustin/go-humanize v1.0.0
|
||||
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.49 h1:zP6QvjdRU3UqDZt3rEqjkR/5M68XRVms7htHfE9tLOc=
|
||||
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/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
|
||||
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
|
||||
}
|
123
internal/discord/category/category.go
Normal file
123
internal/discord/category/category.go
Normal file
|
@ -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
|
||||
}
|
97
internal/discord/channel/actioner.go
Normal file
97
internal/discord/channel/actioner.go
Normal file
|
@ -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)
|
||||
}
|
52
internal/discord/channel/backlogger.go
Normal file
52
internal/discord/channel/backlogger.go
Normal file
|
@ -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
|
||||
}
|
65
internal/discord/channel/channel.go
Normal file
65
internal/discord/channel/channel.go
Normal file
|
@ -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 (
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"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"
|
||||
)
|
||||
|
||||
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 {
|
||||
m, err := s.Store.Member(g.ID, u.ID)
|
||||
if err == nil {
|
||||
return cchat.CompletionEntry{
|
||||
Raw: u.Mention(),
|
||||
Text: RenderMemberName(*m, *g, s),
|
||||
Text: message.RenderMemberName(*m, *g, s),
|
||||
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
|
||||
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 word == "" {
|
||||
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.
|
||||
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 {
|
||||
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.
|
||||
m, merr := ch.session.Store.Members(ch.guildID)
|
||||
m, merr := ch.state.Store.Members(ch.guildID)
|
||||
g, gerr := ch.guild()
|
||||
|
||||
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
|
||||
// search for them.
|
||||
if len(m) == 0 {
|
||||
ch.session.MemberState.SearchMember(ch.guildID, word)
|
||||
ch.state.MemberState.SearchMember(ch.guildID, word)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -107,7 +145,7 @@ func (ch *SendableChannel) completeMentions(word string) (entries []cchat.Comple
|
|||
if contains(match, mem.User.Username, mem.Nick) {
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
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},
|
||||
IconURL: mem.User.AvatarURL(),
|
||||
})
|
||||
|
@ -120,7 +158,7 @@ func (ch *SendableChannel) completeMentions(word string) (entries []cchat.Comple
|
|||
return
|
||||
}
|
||||
|
||||
func (ch *SendableChannel) completeChannels(word string) (entries []cchat.CompletionEntry) {
|
||||
func (ch *Channel) completeChannels(word string) (entries []cchat.CompletionEntry) {
|
||||
// Ignore if empty word.
|
||||
if word == "" {
|
||||
return
|
||||
|
@ -131,7 +169,7 @@ func (ch *SendableChannel) completeChannels(word string) (entries []cchat.Comple
|
|||
return
|
||||
}
|
||||
|
||||
c, err := ch.session.State.Channels(ch.guildID)
|
||||
c, err := ch.state.State.Channels(ch.guildID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -145,7 +183,7 @@ func (ch *SendableChannel) completeChannels(word string) (entries []cchat.Comple
|
|||
|
||||
var category string
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -164,13 +202,13 @@ func (ch *SendableChannel) completeChannels(word string) (entries []cchat.Comple
|
|||
return
|
||||
}
|
||||
|
||||
func (ch *SendableChannel) completeEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||
func (ch *Channel) completeEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||
// Ignore if empty word.
|
||||
if word == "" {
|
||||
return
|
||||
}
|
||||
|
||||
e, err := ch.session.EmojiState.Get(ch.guildID)
|
||||
e, err := ch.state.EmojiState.Get(ch.guildID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
61
internal/discord/channel/editor.go
Normal file
61
internal/discord/channel/editor.go
Normal file
|
@ -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
|
||||
}
|
70
internal/discord/channel/indicators.go
Normal file
70
internal/discord/channel/indicators.go
Normal file
|
@ -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
|
||||
}
|
164
internal/discord/channel/memberlist/member.go
Normal file
164
internal/discord/channel/memberlist/member.go
Normal file
|
@ -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{}
|
||||
}
|
||||
}
|
36
internal/discord/channel/memberlist/memberlist.go
Normal file
36
internal/discord/channel/memberlist/memberlist.go
Normal file
|
@ -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)
|
||||
})
|
||||
}
|
91
internal/discord/channel/memberlist/section.go
Normal file
91
internal/discord/channel/memberlist/section.go
Normal file
|
@ -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
|
||||
}
|
111
internal/discord/channel/memberlister.go
Normal file
111
internal/discord/channel/memberlister.go
Normal file
|
@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
125
internal/discord/channel/messenger.go
Normal file
125
internal/discord/channel/messenger.go
Normal file
|
@ -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
|
||||
}
|
73
internal/discord/channel/nicknamer.go
Normal file
73
internal/discord/channel/nicknamer.go
Normal file
|
@ -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
|
||||
}
|
62
internal/discord/channel/sender.go
Normal file
62
internal/discord/channel/sender.go
Normal file
|
@ -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 (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"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 {
|
||||
Author
|
||||
message.Author
|
||||
time discord.UnixTimestamp
|
||||
}
|
||||
|
||||
var _ cchat.Typer = (*Typer)(nil)
|
||||
|
||||
func NewTyperAuthor(author Author, ev *gateway.TypingStartEvent) Typer {
|
||||
func NewFromAuthor(author message.Author, ev *gateway.TypingStartEvent) Typer {
|
||||
return Typer{
|
||||
Author: author,
|
||||
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() {
|
||||
g, err := s.Store.Guild(ev.GuildID)
|
||||
if err != nil {
|
||||
|
@ -38,7 +40,7 @@ func NewTyper(s *Session, ev *gateway.TypingStartEvent) (*Typer, error) {
|
|||
}
|
||||
|
||||
return &Typer{
|
||||
Author: NewGuildMember(*ev.Member, *g, s),
|
||||
Author: message.NewGuildMember(*ev.Member, *g, s),
|
||||
time: ev.Timestamp,
|
||||
}, nil
|
||||
}
|
||||
|
@ -51,7 +53,7 @@ func NewTyper(s *Session, ev *gateway.TypingStartEvent) (*Typer, error) {
|
|||
for _, user := range c.DMRecipients {
|
||||
if user.ID == ev.UserID {
|
||||
return &Typer{
|
||||
Author: NewUser(user, s),
|
||||
Author: message.NewUser(user, s),
|
||||
time: ev.Timestamp,
|
||||
}, nil
|
||||
}
|
82
internal/discord/folder/folder.go
Normal file
82
internal/discord/folder/folder.go
Normal file
|
@ -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
|
||||
}
|
131
internal/discord/guild/guild.go
Normal file
131
internal/discord/guild/guild.go
Normal file
|
@ -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
|
||||
}
|
90
internal/discord/message/author.go
Normal file
90
internal/discord/message/author.go
Normal file
|
@ -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 (
|
||||
"time"
|
||||
|
@ -6,8 +6,8 @@ import (
|
|||
"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-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
|
@ -52,91 +52,6 @@ func (m messageHeader) Time() 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 {
|
||||
messageHeader
|
||||
|
||||
|
@ -147,7 +62,7 @@ type Message struct {
|
|||
mentioned bool
|
||||
}
|
||||
|
||||
func NewMessageUpdateContent(msg discord.Message, s *Session) Message {
|
||||
func NewMessageUpdateContent(msg discord.Message, s *state.Instance) Message {
|
||||
// Check if content is empty.
|
||||
if msg.Content == "" {
|
||||
// Then grab the content from the state.
|
||||
|
@ -164,7 +79,7 @@ func NewMessageUpdateContent(msg discord.Message, s *Session) Message {
|
|||
}
|
||||
|
||||
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{
|
||||
messageHeader: newHeader(msg),
|
||||
|
@ -174,7 +89,7 @@ func NewMessageUpdateAuthor(
|
|||
|
||||
// NewMessageCreate uses the session to create a message. It does not do
|
||||
// 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.
|
||||
g, err := s.Store.Guild(c.GuildID)
|
||||
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
|
||||
// backlog. It takes in an existing guild and tries to fetch a new member, if
|
||||
// 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
|
||||
// complicated member fetching process.
|
||||
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))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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.
|
||||
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() {
|
||||
return nil
|
||||
}
|
155
internal/discord/session/session.go
Normal file
155
internal/discord/session/session.go
Normal file
|
@ -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
|
||||
}
|
62
internal/discord/state/state.go
Normal file
62
internal/discord/state/state.go
Normal file
|
@ -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)
|
||||
}
|
19
internal/funcutil/funcutil.go
Normal file
19
internal/funcutil/funcutil.go
Normal file
|
@ -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/state"
|
||||
"github.com/diamondburned/cchat-discord/urlutils"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"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/state"
|
||||
"github.com/diamondburned/cchat-discord/urlutils"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen"
|
||||
"github.com/diamondburned/ningen/md"
|
|
@ -16,6 +16,10 @@ func AvatarURL(URL string) string {
|
|||
|
||||
// Sized wraps the URL with the size query.
|
||||
func Sized(URL string, size int) string {
|
||||
if URL == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
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 a new issue