Upgraded to cchat v2
This commit is contained in:
parent
e9796170f8
commit
da520786d7
69
discord.go
69
discord.go
|
@ -1,79 +1,36 @@
|
||||||
package discord
|
package discord
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/authenticate"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/session"
|
"github.com/diamondburned/cchat-discord/internal/discord/session"
|
||||||
"github.com/diamondburned/cchat/services"
|
"github.com/diamondburned/cchat/services"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/pkg/errors"
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var service cchat.Service = Service{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
services.RegisterService(&Service{})
|
services.RegisterService(service)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
type Service struct {
|
||||||
var ErrInvalidSession = errors.New("invalid session")
|
empty.Service
|
||||||
|
}
|
||||||
type Service struct{}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ cchat.Iconer = (*Service)(nil)
|
|
||||||
_ cchat.Service = (*Service)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (Service) Name() text.Rich {
|
func (Service) Name() text.Rich {
|
||||||
return text.Rich{Content: "Discord"}
|
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 {
|
func (Service) Authenticate() cchat.Authenticator {
|
||||||
return &Authenticator{}
|
return authenticate.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) RestoreSession(data map[string]string) (cchat.Session, error) {
|
func (Service) AsIconer() cchat.Iconer {
|
||||||
tk, ok := data["token"]
|
return Logo
|
||||||
if !ok {
|
|
||||||
return nil, ErrInvalidSession
|
|
||||||
}
|
|
||||||
|
|
||||||
return session.NewFromToken(tk)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Authenticator struct{}
|
func (Service) AsSessionRestorer() cchat.SessionRestorer {
|
||||||
|
return session.Restorer
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
package authenticate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/session"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMalformed = errors.New("malformed authentication form")
|
||||||
|
EnterPassword = errors.New("enter your password")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Authenticator struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() cchat.Authenticator {
|
||||||
|
return &Authenticator{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) stage() int {
|
||||||
|
switch {
|
||||||
|
// Stage 1: Prompt for the token OR username.
|
||||||
|
case a.username == "" && a.password == "":
|
||||||
|
return 0
|
||||||
|
|
||||||
|
// Stage 2: Prompt for the password.
|
||||||
|
case a.password == "":
|
||||||
|
return 1
|
||||||
|
|
||||||
|
// Stage 3: Prompt for the TOTP token.
|
||||||
|
default:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) AuthenticateForm() []cchat.AuthenticateEntry {
|
||||||
|
switch a.stage() {
|
||||||
|
case 0:
|
||||||
|
return []cchat.AuthenticateEntry{
|
||||||
|
{Name: "Token", Secret: true},
|
||||||
|
{Name: "Username", Description: "Fill either Token or Username only."},
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
return []cchat.AuthenticateEntry{
|
||||||
|
{Name: "Password", Secret: true},
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
return []cchat.AuthenticateEntry{
|
||||||
|
{Name: "Auth Code", Description: "6-digit code for Two-factor Authentication."},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) Authenticate(form []string) (cchat.Session, error) {
|
||||||
|
switch a.stage() {
|
||||||
|
case 0:
|
||||||
|
if len(form) != 2 {
|
||||||
|
return nil, ErrMalformed
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case form[0] != "": // Token
|
||||||
|
i, err := state.NewFromToken(form[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.NewFromInstance(i)
|
||||||
|
|
||||||
|
case form[1] != "": // Username
|
||||||
|
// Move to a new stage.
|
||||||
|
a.username = form[1]
|
||||||
|
return nil, EnterPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
if len(form) != 1 {
|
||||||
|
return nil, ErrMalformed
|
||||||
|
}
|
||||||
|
|
||||||
|
a.password = form[0]
|
||||||
|
|
||||||
|
i, err := state.Login(a.username, a.password, "")
|
||||||
|
if err != nil {
|
||||||
|
// If the error is not ErrMFA, then we should reset password to
|
||||||
|
// empty.
|
||||||
|
if !errors.Is(err, session.ErrMFA) {
|
||||||
|
a.password = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.NewFromInstance(i)
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
if len(form) != 1 {
|
||||||
|
return nil, ErrMalformed
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := state.Login(a.username, a.password, form[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.NewFromInstance(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrMalformed
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/channel"
|
"github.com/diamondburned/cchat-discord/internal/discord/channel"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -57,17 +58,13 @@ func FilterCategory(chs []discord.Channel, catID discord.ChannelID) []discord.Ch
|
||||||
}
|
}
|
||||||
|
|
||||||
type Category struct {
|
type Category struct {
|
||||||
|
empty.Server
|
||||||
id discord.ChannelID
|
id discord.ChannelID
|
||||||
guildID discord.GuildID
|
guildID discord.GuildID
|
||||||
state *state.Instance
|
state *state.Instance
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func New(s *state.Instance, ch discord.Channel) cchat.Server {
|
||||||
_ cchat.Server = (*Category)(nil)
|
|
||||||
_ cchat.Lister = (*Category)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(s *state.Instance, ch discord.Channel) *Category {
|
|
||||||
return &Category{
|
return &Category{
|
||||||
id: ch.ID,
|
id: ch.ID,
|
||||||
guildID: ch.GuildID,
|
guildID: ch.GuildID,
|
||||||
|
@ -91,9 +88,7 @@ func (c *Category) Name() text.Rich {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Category) IsLister() bool {
|
func (c *Category) AsLister() cchat.Lister { return c }
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Category) Servers(container cchat.ServersContainer) error {
|
func (c *Category) Servers(container cchat.ServersContainer) error {
|
||||||
t, err := c.state.Channels(c.guildID)
|
t, err := c.state.Channels(c.guildID)
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -3,15 +3,17 @@ package channel
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/message"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
id discord.ChannelID
|
*empty.Server
|
||||||
guildID discord.GuildID
|
*shared.Channel
|
||||||
state *state.Instance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cchat.Server = (*Channel)(nil)
|
var _ cchat.Server = (*Channel)(nil)
|
||||||
|
@ -23,38 +25,40 @@ func New(s *state.Instance, ch discord.Channel) (cchat.Server, error) {
|
||||||
return nil, errors.Wrap(err, "Failed to get permission")
|
return nil, errors.Wrap(err, "Failed to get permission")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Channel{
|
return Channel{
|
||||||
id: ch.ID,
|
Channel: &shared.Channel{
|
||||||
guildID: ch.GuildID,
|
ID: ch.ID,
|
||||||
state: s,
|
GuildID: ch.GuildID,
|
||||||
|
State: s,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// self does not do IO.
|
// self does not do IO.
|
||||||
func (ch *Channel) self() (*discord.Channel, error) {
|
func (ch Channel) self() (*discord.Channel, error) {
|
||||||
return ch.state.Store.Channel(ch.id)
|
return ch.State.Store.Channel(ch.Channel.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// messages does not do IO.
|
// messages does not do IO.
|
||||||
func (ch *Channel) messages() ([]discord.Message, error) {
|
func (ch Channel) messages() ([]discord.Message, error) {
|
||||||
return ch.state.Store.Messages(ch.id)
|
return ch.State.Store.Messages(ch.Channel.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) guild() (*discord.Guild, error) {
|
func (ch Channel) guild() (*discord.Guild, error) {
|
||||||
if ch.guildID.IsValid() {
|
if ch.GuildID.IsValid() {
|
||||||
return ch.state.Store.Guild(ch.guildID)
|
return ch.State.Store.Guild(ch.GuildID)
|
||||||
}
|
}
|
||||||
return nil, errors.New("channel not in a guild")
|
return nil, errors.New("channel not in a guild")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) ID() cchat.ID {
|
func (ch Channel) ID() cchat.ID {
|
||||||
return ch.id.String()
|
return ch.Channel.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) Name() text.Rich {
|
func (ch Channel) Name() text.Rich {
|
||||||
c, err := ch.self()
|
c, err := ch.self()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return text.Rich{Content: ch.id.String()}
|
return text.Rich{Content: ch.Channel.ID.String()}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.NSFW {
|
if c.NSFW {
|
||||||
|
@ -63,3 +67,11 @@ func (ch *Channel) Name() text.Rich {
|
||||||
return text.Rich{Content: "#" + c.Name}
|
return text.Rich{Content: "#" + c.Name}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ch Channel) AsMessenger() cchat.Messenger {
|
||||||
|
if !ch.HasPermission(discord.PermissionViewChannel) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.New(ch.Channel)
|
||||||
|
}
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,15 +1,21 @@
|
||||||
package channel
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ cchat.Actioner = (*Channel)(nil)
|
type Actioner struct {
|
||||||
|
*shared.Channel
|
||||||
|
}
|
||||||
|
|
||||||
// IsActioner returns true.
|
var _ cchat.Actioner = (*Actioner)(nil)
|
||||||
func (ch *Channel) IsActioner() bool { return true }
|
|
||||||
|
func New(ch *shared.Channel) Actioner {
|
||||||
|
return Actioner{ch}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ActionDelete = "Delete"
|
ActionDelete = "Delete"
|
||||||
|
@ -17,7 +23,7 @@ const (
|
||||||
|
|
||||||
var ErrUnknownAction = errors.New("unknown message action")
|
var ErrUnknownAction = errors.New("unknown message action")
|
||||||
|
|
||||||
func (ch *Channel) DoMessageAction(action, id string) error {
|
func (ac Actioner) DoAction(action, id string) error {
|
||||||
s, err := discord.ParseSnowflake(id)
|
s, err := discord.ParseSnowflake(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Failed to parse ID")
|
return errors.Wrap(err, "Failed to parse ID")
|
||||||
|
@ -25,25 +31,25 @@ func (ch *Channel) DoMessageAction(action, id string) error {
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case ActionDelete:
|
case ActionDelete:
|
||||||
return ch.state.DeleteMessage(ch.id, discord.MessageID(s))
|
return ac.State.DeleteMessage(ac.ID, discord.MessageID(s))
|
||||||
default:
|
default:
|
||||||
return ErrUnknownAction
|
return ErrUnknownAction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) MessageActions(id string) []string {
|
func (ac Actioner) Actions(id string) []string {
|
||||||
s, err := discord.ParseSnowflake(id)
|
s, err := discord.ParseSnowflake(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := ch.state.Store.Message(ch.id, discord.MessageID(s))
|
m, err := ac.State.Store.Message(ac.ID, discord.MessageID(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current user.
|
// Get the current user.
|
||||||
u, err := ch.state.Store.Me()
|
u, err := ac.State.Store.Me()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -54,7 +60,7 @@ func (ch *Channel) MessageActions(id string) []string {
|
||||||
// We also can if we have the Manage Messages permission, which would allow
|
// We also can if we have the Manage Messages permission, which would allow
|
||||||
// us to delete others' messages.
|
// us to delete others' messages.
|
||||||
if !canDelete {
|
if !canDelete {
|
||||||
canDelete = ch.canManageMessages(u.ID)
|
canDelete = ac.canManageMessages(u.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if canDelete {
|
if canDelete {
|
||||||
|
@ -66,26 +72,26 @@ func (ch *Channel) MessageActions(id string) []string {
|
||||||
|
|
||||||
// canManageMessages returns whether or not the user is allowed to manage
|
// canManageMessages returns whether or not the user is allowed to manage
|
||||||
// messages.
|
// messages.
|
||||||
func (ch *Channel) canManageMessages(userID discord.UserID) bool {
|
func (ac Actioner) canManageMessages(userID discord.UserID) bool {
|
||||||
// If we're not in a guild, then clearly we cannot.
|
// If we're not in a guild, then clearly we cannot.
|
||||||
if !ch.guildID.IsValid() {
|
if !ac.GuildID.IsValid() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need the guild, member and channel to calculate the permission
|
// We need the guild, member and channel to calculate the permission
|
||||||
// overrides.
|
// overrides.
|
||||||
|
|
||||||
g, err := ch.guild()
|
g, err := ac.Guild()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := ch.self()
|
c, err := ac.Self()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := ch.state.Store.Member(ch.guildID, userID)
|
m, err := ac.State.Store.Member(ac.GuildID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package backlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/message"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Backlogger struct {
|
||||||
|
*shared.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ch *shared.Channel) cchat.Backlogger {
|
||||||
|
return Backlogger{ch}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bl Backlogger) MessagesBefore(
|
||||||
|
ctx context.Context,
|
||||||
|
b cchat.ID,
|
||||||
|
c cchat.MessagesContainer) error {
|
||||||
|
|
||||||
|
p, err := discord.ParseSnowflake(b)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to parse snowflake")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := bl.State.WithContext(ctx)
|
||||||
|
|
||||||
|
m, err := s.MessagesBefore(bl.ID, discord.MessageID(p), uint(bl.State.MaxMessages()))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to get messages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the backlog without any member information.
|
||||||
|
g, err := s.Guild(bl.GuildID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Failed to get guild")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range m {
|
||||||
|
// Discord sucks.
|
||||||
|
m.GuildID = bl.GuildID
|
||||||
|
|
||||||
|
c.CreateMessage(message.NewBacklogMessage(m, bl.State, *g))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,47 +1,44 @@
|
||||||
package channel
|
package edit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ cchat.Editor = (*Channel)(nil)
|
type Editor struct {
|
||||||
|
*shared.Channel
|
||||||
|
}
|
||||||
|
|
||||||
// IsEditor returns true if the user can send messages in this channel.
|
func New(ch *shared.Channel) cchat.Editor {
|
||||||
func (ch *Channel) IsEditor() bool {
|
return Editor{ch}
|
||||||
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
|
// MessageEditable returns true if the given message ID belongs to the current
|
||||||
// user.
|
// user.
|
||||||
func (ch *Channel) MessageEditable(id string) bool {
|
func (ed Editor) MessageEditable(id string) bool {
|
||||||
s, err := discord.ParseSnowflake(id)
|
s, err := discord.ParseSnowflake(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := ch.state.Store.Message(ch.id, discord.MessageID(s))
|
m, err := ed.State.Store.Message(ed.ID, discord.MessageID(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.Author.ID == ch.state.UserID
|
return m.Author.ID == ed.State.UserID
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawMessageContent returns the raw message content from Discord.
|
// RawMessageContent returns the raw message content from Discord.
|
||||||
func (ch *Channel) RawMessageContent(id string) (string, error) {
|
func (ed Editor) RawMessageContent(id string) (string, error) {
|
||||||
s, err := discord.ParseSnowflake(id)
|
s, err := discord.ParseSnowflake(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "Failed to parse ID")
|
return "", errors.Wrap(err, "Failed to parse ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := ch.state.Store.Message(ch.id, discord.MessageID(s))
|
m, err := ed.State.Store.Message(ed.ID, discord.MessageID(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "Failed to get the message")
|
return "", errors.Wrap(err, "Failed to get the message")
|
||||||
}
|
}
|
||||||
|
@ -50,12 +47,12 @@ func (ch *Channel) RawMessageContent(id string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditMessage edits the message to the given content string.
|
// EditMessage edits the message to the given content string.
|
||||||
func (ch *Channel) EditMessage(id, content string) error {
|
func (ed Editor) EditMessage(id, content string) error {
|
||||||
s, err := discord.ParseSnowflake(id)
|
s, err := discord.ParseSnowflake(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Failed to parse ID")
|
return errors.Wrap(err, "Failed to parse ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = ch.state.EditText(ch.id, discord.MessageID(s), content)
|
_, err = ed.State.EditText(ed.ID, discord.MessageID(s), content)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package indicate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/typer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypingIndicator struct {
|
||||||
|
*shared.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTyping(ch *shared.Channel) cchat.TypingIndicator {
|
||||||
|
return TypingIndicator{ch}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti TypingIndicator) Typing() error {
|
||||||
|
return ti.State.Typing(ti.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypingTimeout returns 10 seconds.
|
||||||
|
func (ti TypingIndicator) TypingTimeout() time.Duration {
|
||||||
|
return 10 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti TypingIndicator) TypingSubscribe(tc cchat.TypingContainer) (func(), error) {
|
||||||
|
return ti.State.AddHandler(func(t *gateway.TypingStartEvent) {
|
||||||
|
// Ignore channel mismatch or if the typing event is ours.
|
||||||
|
if t.ChannelID != ti.ID || t.UserID == ti.State.UserID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if typer, err := typer.New(ti.State, t); err == nil {
|
||||||
|
tc.AddTyper(typer)
|
||||||
|
}
|
||||||
|
}), nil
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package indicate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
|
"github.com/diamondburned/ningen/states/read"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnreadIndicator struct {
|
||||||
|
*shared.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUnread(ch *shared.Channel) cchat.UnreadIndicator {
|
||||||
|
return UnreadIndicator{ch}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muted returns if this channel is muted. This includes the channel's category
|
||||||
|
// and guild.
|
||||||
|
func (ui UnreadIndicator) Muted() bool {
|
||||||
|
return (ui.GuildID.IsValid() && ui.State.MutedState.Guild(ui.GuildID, false)) ||
|
||||||
|
ui.State.MutedState.Channel(ui.ID) ||
|
||||||
|
ui.State.MutedState.Category(ui.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui UnreadIndicator) UnreadIndicate(indicator cchat.UnreadContainer) (func(), error) {
|
||||||
|
if rs := ui.State.ReadState.FindLast(ui.ID); rs != nil {
|
||||||
|
c, err := ui.Self()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Failed to get self channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LastMessageID > rs.LastMessageID && !ui.Muted() {
|
||||||
|
indicator.SetUnread(true, rs.MentionCount > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui.State.ReadState.OnUpdate(func(ev *read.UpdateEvent) {
|
||||||
|
if ui.ID == ev.ChannelID && !ui.Muted() {
|
||||||
|
indicator.SetUnread(ev.Unread, ev.MentionCount > 0)
|
||||||
|
}
|
||||||
|
}), nil
|
||||||
|
}
|
|
@ -8,29 +8,24 @@ import (
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
"github.com/diamondburned/cchat-discord/internal/segments"
|
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/segments/emoji"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/segments/mention"
|
||||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Member struct {
|
type Member struct {
|
||||||
Channel
|
channel *shared.Channel
|
||||||
state *state.Instance
|
|
||||||
|
|
||||||
userID discord.UserID
|
userID discord.UserID
|
||||||
origName string // use if cache is stale
|
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.
|
// New creates a new list member. it.Member must not be nil.
|
||||||
func (c Channel) NewMember(opItem gateway.GuildMemberListOpItem) *Member {
|
func NewMember(ch *shared.Channel, opItem gateway.GuildMemberListOpItem) cchat.ListMember {
|
||||||
return &Member{
|
return &Member{
|
||||||
Channel: c,
|
channel: ch,
|
||||||
userID: opItem.Member.User.ID,
|
userID: opItem.Member.User.ID,
|
||||||
origName: opItem.Member.User.Username,
|
origName: opItem.Member.User.Username,
|
||||||
}
|
}
|
||||||
|
@ -41,12 +36,12 @@ func (l *Member) ID() cchat.ID {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Member) Name() text.Rich {
|
func (l *Member) Name() text.Rich {
|
||||||
g, err := l.state.Store.Guild(l.guildID)
|
g, err := l.channel.State.Store.Guild(l.channel.GuildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return text.Plain(l.origName)
|
return text.Plain(l.origName)
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := l.state.Store.Member(l.guildID, l.userID)
|
m, err := l.channel.State.Store.Member(l.channel.GuildID, l.userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return text.Plain(l.origName)
|
return text.Plain(l.origName)
|
||||||
}
|
}
|
||||||
|
@ -56,8 +51,8 @@ func (l *Member) Name() text.Rich {
|
||||||
name = m.Nick
|
name = m.Nick
|
||||||
}
|
}
|
||||||
|
|
||||||
mention := segments.MemberSegment(0, len(name), *g, *m)
|
mention := mention.MemberSegment(0, len(name), *g, *m)
|
||||||
mention.WithState(l.state.State)
|
mention.WithState(l.channel.State.State)
|
||||||
|
|
||||||
var txt = text.Rich{
|
var txt = text.Rich{
|
||||||
Content: name,
|
Content: name,
|
||||||
|
@ -65,17 +60,16 @@ func (l *Member) Name() text.Rich {
|
||||||
}
|
}
|
||||||
|
|
||||||
if c := discord.MemberColor(*g, *m); c != discord.DefaultMemberColor {
|
if c := discord.MemberColor(*g, *m); c != discord.DefaultMemberColor {
|
||||||
txt.Segments = append(txt.Segments, segments.NewColored(len(name), uint32(c)))
|
txt.Segments = append(txt.Segments, colored.New(len(name), uint32(c)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return txt
|
return txt
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsIconer returns true.
|
func (l *Member) AsIconer() cchat.Iconer { return l }
|
||||||
func (l *Member) IsIconer() bool { return true }
|
|
||||||
|
|
||||||
func (l *Member) Icon(ctx context.Context, c cchat.IconContainer) (func(), error) {
|
func (l *Member) Icon(ctx context.Context, c cchat.IconContainer) (func(), error) {
|
||||||
m, err := l.state.Member(l.guildID, l.userID)
|
m, err := l.channel.State.Member(l.channel.GuildID, l.userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -85,28 +79,28 @@ func (l *Member) Icon(ctx context.Context, c cchat.IconContainer) (func(), error
|
||||||
return func() {}, nil
|
return func() {}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Member) Status() cchat.UserStatus {
|
func (l *Member) Status() cchat.Status {
|
||||||
p, err := l.state.Store.Presence(l.guildID, l.userID)
|
p, err := l.channel.State.Store.Presence(l.channel.GuildID, l.userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cchat.UnknownStatus
|
return cchat.StatusUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
switch p.Status {
|
switch p.Status {
|
||||||
case discord.OnlineStatus:
|
case discord.OnlineStatus:
|
||||||
return cchat.OnlineStatus
|
return cchat.StatusOnline
|
||||||
case discord.DoNotDisturbStatus:
|
case discord.DoNotDisturbStatus:
|
||||||
return cchat.BusyStatus
|
return cchat.StatusBusy
|
||||||
case discord.IdleStatus:
|
case discord.IdleStatus:
|
||||||
return cchat.AwayStatus
|
return cchat.StatusAway
|
||||||
case discord.OfflineStatus, discord.InvisibleStatus:
|
case discord.OfflineStatus, discord.InvisibleStatus:
|
||||||
return cchat.OfflineStatus
|
return cchat.StatusOffline
|
||||||
default:
|
default:
|
||||||
return cchat.UnknownStatus
|
return cchat.StatusUnknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Member) Secondary() text.Rich {
|
func (l *Member) Secondary() text.Rich {
|
||||||
p, err := l.state.Store.Presence(l.guildID, l.userID)
|
p, err := l.channel.State.Store.Presence(l.channel.GuildID, l.userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return text.Plain("")
|
return text.Plain("")
|
||||||
}
|
}
|
||||||
|
@ -142,11 +136,9 @@ func formatSmallActivity(ac discord.Activity) text.Rich {
|
||||||
status.WriteString(ac.Emoji.Name)
|
status.WriteString(ac.Emoji.Name)
|
||||||
status.WriteByte(' ')
|
status.WriteByte(' ')
|
||||||
} else {
|
} else {
|
||||||
segmts = append(segmts, segments.EmojiSegment{
|
segmts = append(segmts, emoji.Segment{
|
||||||
Start: status.Len(),
|
Start: status.Len(),
|
||||||
Name: ac.Emoji.Name,
|
Emoji: emoji.EmojiFromDiscord(*ac.Emoji, ac.State == ""),
|
||||||
EmojiURL: ac.Emoji.EmojiURL() + "?size=64",
|
|
||||||
Large: ac.State == "",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
package channel
|
package memberlist
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/memberlist"
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
"github.com/diamondburned/ningen/states/member"
|
"github.com/diamondburned/ningen/states/member"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,24 +30,21 @@ func seekPrevGroup(l *member.List, ix int) (item, group gateway.GuildMemberListO
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cchat.MemberLister = (*Channel)(nil)
|
type MemberLister struct {
|
||||||
|
*shared.Channel
|
||||||
// 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 {
|
func New(ch *shared.Channel) cchat.MemberLister {
|
||||||
return memberlist.NewChannel(ch.state, ch.id, ch.guildID)
|
return MemberLister{ch}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) ListMembers(ctx context.Context, c cchat.MemberListContainer) (func(), error) {
|
func (ml MemberLister) ListMembers(ctx context.Context, c cchat.MemberListContainer) (func(), error) {
|
||||||
if !ch.guildID.IsValid() {
|
if !ml.GuildID.IsValid() {
|
||||||
return func() {}, nil
|
return func() {}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel := ch.state.AddHandler(func(u *gateway.GuildMemberListUpdate) {
|
cancel := ml.State.AddHandler(func(u *gateway.GuildMemberListUpdate) {
|
||||||
l, err := ch.state.MemberState.GetMemberList(ch.guildID, ch.id)
|
l, err := ml.State.MemberState.GetMemberList(ml.GuildID, ml.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return // wat
|
return // wat
|
||||||
}
|
}
|
||||||
|
@ -56,44 +53,41 @@ func (ch *Channel) ListMembers(ctx context.Context, c cchat.MemberListContainer)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var listCh = ch.memberListCh()
|
|
||||||
|
|
||||||
for _, ev := range u.Ops {
|
for _, ev := range u.Ops {
|
||||||
switch ev.Op {
|
switch ev.Op {
|
||||||
case "SYNC":
|
case "SYNC":
|
||||||
ch.checkSync(c)
|
ml.checkSync(c)
|
||||||
|
|
||||||
case "INSERT", "UPDATE":
|
case "INSERT", "UPDATE":
|
||||||
item, group := seekPrevGroup(l, ev.Index)
|
item, group := seekPrevGroup(l, ev.Index)
|
||||||
if item.Member != nil && group.Group != nil {
|
if item.Member != nil && group.Group != nil {
|
||||||
c.SetMember(group.Group.ID, listCh.NewMember(item))
|
c.SetMember(group.Group.ID, NewMember(ml.Channel, item))
|
||||||
listCh.FlushMemberGroups(l, c)
|
ml.FlushMemberGroups(l, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
_, group := seekPrevGroup(l, ev.Index-1)
|
_, group := seekPrevGroup(l, ev.Index-1)
|
||||||
if group.Group != nil && ev.Item.Member != nil {
|
if group.Group != nil && ev.Item.Member != nil {
|
||||||
c.RemoveMember(group.Group.ID, ev.Item.Member.User.ID.String())
|
c.RemoveMember(group.Group.ID, ev.Item.Member.User.ID.String())
|
||||||
listCh.FlushMemberGroups(l, c)
|
ml.FlushMemberGroups(l, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ch.checkSync(c)
|
ml.checkSync(c)
|
||||||
|
|
||||||
return cancel, nil
|
return cancel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) checkSync(c cchat.MemberListContainer) {
|
func (ml MemberLister) checkSync(c cchat.MemberListContainer) {
|
||||||
l, err := ch.state.MemberState.GetMemberList(ch.guildID, ch.id)
|
l, err := ml.State.MemberState.GetMemberList(ml.GuildID, ml.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch.state.MemberState.RequestMemberList(ch.guildID, ch.id, 0)
|
ml.State.MemberState.RequestMemberList(ml.GuildID, ml.ID, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
listCh := ch.memberListCh()
|
ml.FlushMemberGroups(l, c)
|
||||||
listCh.FlushMemberGroups(l, c)
|
|
||||||
|
|
||||||
l.ViewItems(func(items []gateway.GuildMemberListOpItem) {
|
l.ViewItems(func(items []gateway.GuildMemberListOpItem) {
|
||||||
var group gateway.GuildMemberListGroup
|
var group gateway.GuildMemberListGroup
|
||||||
|
@ -104,8 +98,19 @@ func (ch *Channel) checkSync(c cchat.MemberListContainer) {
|
||||||
group = *item.Group
|
group = *item.Group
|
||||||
|
|
||||||
case item.Member != nil:
|
case item.Member != nil:
|
||||||
c.SetMember(group.ID, listCh.NewMember(item))
|
c.SetMember(group.ID, NewMember(ml.Channel, item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ml MemberLister) 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] = NewSection(ml.Channel, l.ID(), group)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetSections(sections)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
package memberlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Section struct {
|
||||||
|
empty.Namer
|
||||||
|
|
||||||
|
// constant states
|
||||||
|
listID string
|
||||||
|
id string // roleID or online or offline
|
||||||
|
name string
|
||||||
|
total int
|
||||||
|
dynsec DynamicSection
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSection(
|
||||||
|
ch *shared.Channel,
|
||||||
|
listID string,
|
||||||
|
group gateway.GuildMemberListGroup) cchat.MemberSection {
|
||||||
|
|
||||||
|
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{
|
||||||
|
listID: listID,
|
||||||
|
id: group.ID,
|
||||||
|
name: name,
|
||||||
|
total: int(group.Count),
|
||||||
|
dynsec: DynamicSection{
|
||||||
|
Channel: ch,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) AsMemberDynamicSection() cchat.MemberDynamicSection {
|
||||||
|
return s.dynsec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Section) IsMemberDynamicSection() bool { return true }
|
||||||
|
|
||||||
|
type DynamicSection struct {
|
||||||
|
*shared.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.MemberDynamicSection = (*DynamicSection)(nil)
|
||||||
|
|
||||||
|
// TODO: document that Load{More,Less} works more like a shifting window.
|
||||||
|
|
||||||
|
func (s DynamicSection) LoadMore() bool {
|
||||||
|
chunk := s.State.MemberState.GetMemberListChunk(s.GuildID, s.Channel.ID)
|
||||||
|
if chunk < 0 {
|
||||||
|
chunk = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.State.MemberState.RequestMemberList(s.GuildID, s.Channel.ID, chunk) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DynamicSection) LoadLess() bool {
|
||||||
|
chunk := s.State.MemberState.GetMemberListChunk(s.GuildID, s.Channel.ID)
|
||||||
|
if chunk <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.State.MemberState.RequestMemberList(s.GuildID, s.Channel.ID, chunk-1)
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/action"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/backlog"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/edit"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/indicate"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/memberlist"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/nickname"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/message"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/funcutil"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Messenger struct {
|
||||||
|
*empty.Messenger
|
||||||
|
*shared.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Messenger = (*Messenger)(nil)
|
||||||
|
|
||||||
|
func New(ch *shared.Channel) Messenger {
|
||||||
|
return Messenger{Channel: ch}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgr Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
|
||||||
|
state := msgr.State.WithContext(ctx)
|
||||||
|
|
||||||
|
m, err := state.Messages(msgr.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var addcancel = funcutil.NewCancels()
|
||||||
|
|
||||||
|
var constructor func(discord.Message) cchat.MessageCreate
|
||||||
|
|
||||||
|
if msgr.GuildID.IsValid() {
|
||||||
|
// Create the backlog without any member information.
|
||||||
|
g, err := state.Guild(msgr.GuildID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Failed to get guild")
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor = func(m discord.Message) cchat.MessageCreate {
|
||||||
|
return message.NewBacklogMessage(m, msgr.State, *g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to typing events.
|
||||||
|
msgr.State.MemberState.Subscribe(msgr.GuildID)
|
||||||
|
|
||||||
|
// Listen to new members before creating the backlog and requesting members.
|
||||||
|
addcancel(msgr.State.AddHandler(func(c *gateway.GuildMembersChunkEvent) {
|
||||||
|
if c.GuildID != msgr.GuildID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := msgr.Messages()
|
||||||
|
if err != nil {
|
||||||
|
// TODO: log
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g, err := msgr.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, msgr.State))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
constructor = func(m discord.Message) cchat.MessageCreate {
|
||||||
|
return message.NewDirectMessage(m, msgr.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.
|
||||||
|
msgr.State.ReadState.MarkRead(msgr.ID, m[len(m)-1].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind the handler.
|
||||||
|
addcancel(
|
||||||
|
msgr.State.AddHandler(func(m *gateway.MessageCreateEvent) {
|
||||||
|
if m.ChannelID == msgr.ID {
|
||||||
|
ct.CreateMessage(message.NewMessageCreate(m, msgr.State))
|
||||||
|
msgr.State.ReadState.MarkRead(msgr.ID, m.ID)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
msgr.State.AddHandler(func(m *gateway.MessageUpdateEvent) {
|
||||||
|
// If the updated content is empty. TODO: add embed support.
|
||||||
|
if m.ChannelID == msgr.ID {
|
||||||
|
ct.UpdateMessage(message.NewMessageUpdateContent(m.Message, msgr.State))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
msgr.State.AddHandler(func(m *gateway.MessageDeleteEvent) {
|
||||||
|
if m.ChannelID == msgr.ID {
|
||||||
|
ct.DeleteMessage(message.NewHeaderDelete(m))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return funcutil.JoinCancels(addcancel()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgr Messenger) AsSender() cchat.Sender {
|
||||||
|
if !msgr.HasPermission(discord.PermissionSendMessages) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return send.New(msgr.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgr Messenger) AsEditor() cchat.Editor {
|
||||||
|
if !msgr.HasPermission(discord.PermissionSendMessages) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return edit.New(msgr.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgr Messenger) AsActioner() cchat.Actioner {
|
||||||
|
return action.New(msgr.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgr Messenger) AsNicknamer() cchat.Nicknamer {
|
||||||
|
return nickname.New(msgr.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgr Messenger) AsMemberLister() cchat.MemberLister {
|
||||||
|
if !msgr.GuildID.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return memberlist.New(msgr.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgr Messenger) AsBacklogger() cchat.Backlogger {
|
||||||
|
if !msgr.HasPermission(discord.PermissionViewChannel, discord.PermissionReadMessageHistory) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return backlog.New(msgr.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgr Messenger) AsTypingIndicator() cchat.TypingIndicator {
|
||||||
|
return indicate.NewTyping(msgr.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msgr Messenger) AsUnreadIndicator() cchat.UnreadIndicator {
|
||||||
|
return indicate.NewUnread(msgr.Channel)
|
||||||
|
}
|
|
@ -1,37 +1,39 @@
|
||||||
package channel
|
package nickname
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/segments"
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ cchat.Nicknamer = (*Channel)(nil)
|
type Nicknamer struct {
|
||||||
|
*shared.Channel
|
||||||
// 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) {
|
func New(ch *shared.Channel) cchat.Nicknamer {
|
||||||
|
return Nicknamer{ch}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nn Nicknamer) Nickname(ctx context.Context, labeler cchat.LabelContainer) (func(), error) {
|
||||||
// We don't have a nickname if we're not in a guild.
|
// We don't have a nickname if we're not in a guild.
|
||||||
if !ch.guildID.IsValid() {
|
if !nn.GuildID.IsValid() {
|
||||||
return func() {}, nil
|
return func() {}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
state := ch.state.WithContext(ctx)
|
state := nn.State.WithContext(ctx)
|
||||||
|
|
||||||
// MemberColor should fill up the state cache.
|
// MemberColor should fill up the state cache.
|
||||||
c, err := state.MemberColor(ch.guildID, ch.state.UserID)
|
c, err := state.MemberColor(nn.GuildID, nn.State.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Failed to get self member color")
|
return nil, errors.Wrap(err, "Failed to get self member color")
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := state.Member(ch.guildID, ch.state.UserID)
|
m, err := state.Member(nn.GuildID, nn.State.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Failed to get self member")
|
return nil, errors.Wrap(err, "Failed to get self member")
|
||||||
}
|
}
|
||||||
|
@ -42,7 +44,7 @@ func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) (
|
||||||
}
|
}
|
||||||
if c > 0 {
|
if c > 0 {
|
||||||
rich.Segments = []text.Segment{
|
rich.Segments = []text.Segment{
|
||||||
segments.NewColored(len(rich.Content), c.Uint32()),
|
colored.New(len(rich.Content), c.Uint32()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,8 +53,8 @@ func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) (
|
||||||
// Copy the user ID to use.
|
// Copy the user ID to use.
|
||||||
var selfID = m.User.ID
|
var selfID = m.User.ID
|
||||||
|
|
||||||
return ch.state.AddHandler(func(g *gateway.GuildMemberUpdateEvent) {
|
return nn.State.AddHandler(func(g *gateway.GuildMemberUpdateEvent) {
|
||||||
if g.GuildID != ch.guildID || g.User.ID != selfID {
|
if g.GuildID != nn.GuildID || g.User.ID != selfID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,10 +63,10 @@ func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) (
|
||||||
rich.Content = m.Nick
|
rich.Content = m.Nick
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := ch.state.MemberColor(g.GuildID, selfID)
|
c, err := nn.State.MemberColor(g.GuildID, selfID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rich.Segments = []text.Segment{
|
rich.Segments = []text.Segment{
|
||||||
segments.NewColored(len(rich.Content), c.Uint32()),
|
colored.New(len(rich.Content), c.Uint32()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,32 @@
|
||||||
package channel
|
package complete
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/message"
|
"github.com/diamondburned/cchat-discord/internal/discord/message"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Completer struct {
|
||||||
|
*shared.Channel
|
||||||
|
}
|
||||||
|
|
||||||
const MaxCompletion = 15
|
const MaxCompletion = 15
|
||||||
|
|
||||||
var _ cchat.MessageCompleter = (*Channel)(nil)
|
func New(ch *shared.Channel) cchat.Completer {
|
||||||
|
return Completer{ch}
|
||||||
// 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.
|
// CompleteMessage implements message input completion capability for Discord.
|
||||||
// This method supports user mentions, channel mentions and emojis.
|
// This method supports user mentions, channel mentions and emojis.
|
||||||
//
|
//
|
||||||
// For the individual implementations, refer to channel_completion.go.
|
// For the individual implementations, refer to channel_completion.go.
|
||||||
func (ch *Channel) CompleteMessage(words []string, i int) (entries []cchat.CompletionEntry) {
|
func (ch Completer) Complete(words []string, i int64) (entries []cchat.CompletionEntry) {
|
||||||
var word = words[i]
|
var word = words[i]
|
||||||
// Word should have at least a character for the char check.
|
// Word should have at least a character for the char check.
|
||||||
if len(word) < 1 {
|
if len(word) < 1 {
|
||||||
|
@ -70,11 +66,11 @@ func completionUser(s *state.Instance, u discord.User, g *discord.Guild) cchat.C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntry) {
|
func (ch Completer) completeMentions(word string) (entries []cchat.CompletionEntry) {
|
||||||
// If there is no input, then we should grab the latest messages.
|
// If there is no input, then we should grab the latest messages.
|
||||||
if word == "" {
|
if word == "" {
|
||||||
msgs, _ := ch.messages()
|
msgs, _ := ch.State.Store.Messages(ch.ID)
|
||||||
g, _ := ch.guild() // nil is fine
|
g, _ := ch.State.Store.Guild(ch.GuildID) // nil is fine
|
||||||
|
|
||||||
// Keep track of the number of authors.
|
// Keep track of the number of authors.
|
||||||
// TODO: fix excess allocations
|
// TODO: fix excess allocations
|
||||||
|
@ -88,7 +84,7 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
|
||||||
|
|
||||||
// Record the current author and add the entry to the list.
|
// Record the current author and add the entry to the list.
|
||||||
authors[msg.Author.ID] = struct{}{}
|
authors[msg.Author.ID] = struct{}{}
|
||||||
entries = append(entries, completionUser(ch.state, msg.Author, g))
|
entries = append(entries, completionUser(ch.State, msg.Author, g))
|
||||||
|
|
||||||
if len(entries) >= MaxCompletion {
|
if len(entries) >= MaxCompletion {
|
||||||
return
|
return
|
||||||
|
@ -103,8 +99,8 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
|
||||||
var match = strings.ToLower(word)
|
var match = strings.ToLower(word)
|
||||||
|
|
||||||
// If we're not in a guild, then we can check the list of recipients.
|
// If we're not in a guild, then we can check the list of recipients.
|
||||||
if !ch.guildID.IsValid() {
|
if !ch.GuildID.IsValid() {
|
||||||
c, err := ch.self()
|
c, err := ch.State.Store.Channel(ch.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -127,8 +123,8 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're in a guild, then we should search for (all) members.
|
// If we're in a guild, then we should search for (all) members.
|
||||||
m, merr := ch.state.Store.Members(ch.guildID)
|
m, merr := ch.State.Store.Members(ch.GuildID)
|
||||||
g, gerr := ch.guild()
|
g, gerr := ch.State.Store.Guild(ch.GuildID)
|
||||||
|
|
||||||
if merr != nil || gerr != nil {
|
if merr != nil || gerr != nil {
|
||||||
return
|
return
|
||||||
|
@ -137,7 +133,7 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
|
||||||
// If we couldn't find any members, then we can request Discord to
|
// If we couldn't find any members, then we can request Discord to
|
||||||
// search for them.
|
// search for them.
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
ch.state.MemberState.SearchMember(ch.guildID, word)
|
ch.State.MemberState.SearchMember(ch.GuildID, word)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +141,7 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
|
||||||
if contains(match, mem.User.Username, mem.Nick) {
|
if contains(match, mem.User.Username, mem.Nick) {
|
||||||
entries = append(entries, cchat.CompletionEntry{
|
entries = append(entries, cchat.CompletionEntry{
|
||||||
Raw: mem.User.Mention(),
|
Raw: mem.User.Mention(),
|
||||||
Text: message.RenderMemberName(mem, *g, ch.state),
|
Text: message.RenderMemberName(mem, *g, ch.State),
|
||||||
Secondary: text.Rich{Content: mem.User.Username + "#" + mem.User.Discriminator},
|
Secondary: text.Rich{Content: mem.User.Username + "#" + mem.User.Discriminator},
|
||||||
IconURL: mem.User.AvatarURL(),
|
IconURL: mem.User.AvatarURL(),
|
||||||
})
|
})
|
||||||
|
@ -158,18 +154,18 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) completeChannels(word string) (entries []cchat.CompletionEntry) {
|
func (ch Completer) completeChannels(word string) (entries []cchat.CompletionEntry) {
|
||||||
// Ignore if empty word.
|
// Ignore if empty word.
|
||||||
if word == "" {
|
if word == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore if we're not in a guild.
|
// Ignore if we're not in a guild.
|
||||||
if !ch.guildID.IsValid() {
|
if !ch.GuildID.IsValid() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := ch.state.State.Channels(ch.guildID)
|
c, err := ch.State.Store.Channels(ch.GuildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -183,7 +179,7 @@ func (ch *Channel) completeChannels(word string) (entries []cchat.CompletionEntr
|
||||||
|
|
||||||
var category string
|
var category string
|
||||||
if channel.CategoryID.IsValid() {
|
if channel.CategoryID.IsValid() {
|
||||||
if c, _ := ch.state.Store.Channel(channel.CategoryID); c != nil {
|
if c, _ := ch.State.Store.Channel(channel.CategoryID); c != nil {
|
||||||
category = c.Name
|
category = c.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,13 +198,13 @@ func (ch *Channel) completeChannels(word string) (entries []cchat.CompletionEntr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *Channel) completeEmojis(word string) (entries []cchat.CompletionEntry) {
|
func (ch Completer) completeEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||||
// Ignore if empty word.
|
// Ignore if empty word.
|
||||||
if word == "" {
|
if word == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
e, err := ch.state.EmojiState.Get(ch.guildID)
|
e, err := ch.State.EmojiState.Get(ch.GuildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package send
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/arikawa/api"
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sender struct {
|
||||||
|
*shared.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cchat.Sender = (*Sender)(nil)
|
||||||
|
|
||||||
|
func New(ch *shared.Channel) Sender {
|
||||||
|
return Sender{ch}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Sender) Send(msg cchat.SendableMessage) error {
|
||||||
|
var send = api.SendMessageData{Content: msg.Content()}
|
||||||
|
if noncer := msg.AsNoncer(); noncer != nil {
|
||||||
|
send.Nonce = noncer.Nonce()
|
||||||
|
}
|
||||||
|
if attacher := msg.AsAttachments(); attacher != nil {
|
||||||
|
send.Files = addAttachments(attacher.Attachments())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.State.SendMessageComplex(s.ID, send)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAttach returns true if the channel can attach files.
|
||||||
|
func (s Sender) CanAttach() bool {
|
||||||
|
p, err := s.State.StateOnly().Permissions(s.ID, s.State.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Has(discord.PermissionAttachFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Sender) AsCompleter() cchat.Completer {
|
||||||
|
return complete.New(s.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,125 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
ID discord.ChannelID
|
||||||
|
GuildID discord.GuildID
|
||||||
|
State *state.Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPermission returns true if the current user has the given permissions in
|
||||||
|
// the channel.
|
||||||
|
func (ch Channel) HasPermission(perms ...discord.Permissions) bool {
|
||||||
|
p, err := ch.State.StateOnly().Permissions(ch.ID, ch.State.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, perm := range perms {
|
||||||
|
if !p.Has(perm) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch Channel) Messages() ([]discord.Message, error) {
|
||||||
|
return ch.State.Store.Messages(ch.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch Channel) Guild() (*discord.Guild, error) {
|
||||||
|
return ch.State.Store.Guild(ch.GuildID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch Channel) Self() (*discord.Channel, error) {
|
||||||
|
return ch.State.Store.Channel(ch.ID)
|
||||||
|
}
|
|
@ -8,21 +8,18 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/guild"
|
"github.com/diamondburned/cchat-discord/internal/discord/guild"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat-discord/internal/segments"
|
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GuildFolder struct {
|
type GuildFolder struct {
|
||||||
|
empty.Server
|
||||||
gateway.GuildFolder
|
gateway.GuildFolder
|
||||||
state *state.Instance
|
state *state.Instance
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func New(s *state.Instance, gf gateway.GuildFolder) cchat.Server {
|
||||||
_ cchat.Server = (*GuildFolder)(nil)
|
|
||||||
_ cchat.Lister = (*GuildFolder)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(s *state.Instance, gf gateway.GuildFolder) *GuildFolder {
|
|
||||||
// Name should never be empty.
|
// Name should never be empty.
|
||||||
if gf.Name == "" {
|
if gf.Name == "" {
|
||||||
var names = make([]string, 0, len(gf.GuildIDs))
|
var names = make([]string, 0, len(gf.GuildIDs))
|
||||||
|
@ -55,7 +52,7 @@ func (gf *GuildFolder) Name() text.Rich {
|
||||||
if gf.GuildFolder.Color > 0 {
|
if gf.GuildFolder.Color > 0 {
|
||||||
name.Segments = []text.Segment{
|
name.Segments = []text.Segment{
|
||||||
// The length of this black box is actually 3. Mind == blown.
|
// The length of this black box is actually 3. Mind == blown.
|
||||||
segments.NewColored(len(name.Content), gf.GuildFolder.Color.Uint32()),
|
colored.New(len(name.Content), gf.GuildFolder.Color.Uint32()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +60,7 @@ func (gf *GuildFolder) Name() text.Rich {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLister returns true.
|
// IsLister returns true.
|
||||||
func (gf *GuildFolder) IsLister() bool { return true }
|
func (gf *GuildFolder) AsLister() cchat.Lister { return gf }
|
||||||
|
|
||||||
func (gf *GuildFolder) Servers(container cchat.ServersContainer) error {
|
func (gf *GuildFolder) Servers(container cchat.ServersContainer) error {
|
||||||
var servers = make([]cchat.Server, 0, len(gf.GuildIDs))
|
var servers = make([]cchat.Server, 0, len(gf.GuildIDs))
|
||||||
|
|
|
@ -12,28 +12,24 @@ import (
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Guild struct {
|
type Guild struct {
|
||||||
|
empty.Server
|
||||||
id discord.GuildID
|
id discord.GuildID
|
||||||
state *state.Instance
|
state *state.Instance
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func New(s *state.Instance, g *discord.Guild) cchat.Server {
|
||||||
_ cchat.Iconer = (*Guild)(nil)
|
|
||||||
_ cchat.Server = (*Guild)(nil)
|
|
||||||
_ cchat.Lister = (*Guild)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func New(s *state.Instance, g *discord.Guild) *Guild {
|
|
||||||
return &Guild{
|
return &Guild{
|
||||||
id: g.ID,
|
id: g.ID,
|
||||||
state: s,
|
state: s,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFromID(s *state.Instance, gID discord.GuildID) (*Guild, error) {
|
func NewFromID(s *state.Instance, gID discord.GuildID) (cchat.Server, error) {
|
||||||
g, err := s.Guild(gID)
|
g, err := s.Guild(gID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -64,11 +60,7 @@ func (g *Guild) Name() text.Rich {
|
||||||
return text.Rich{Content: s.Name}
|
return text.Rich{Content: s.Name}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsIconer returns true if the guild has an icon.
|
func (g *Guild) AsIconer() cchat.Iconer { return g }
|
||||||
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) {
|
func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||||
s, err := g.self(ctx)
|
s, err := g.self(ctx)
|
||||||
|
@ -89,8 +81,7 @@ func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), e
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLister returns true.
|
func (g *Guild) AsLister() cchat.Lister { return g }
|
||||||
func (g *Guild) IsLister() bool { return true }
|
|
||||||
|
|
||||||
func (g *Guild) Servers(container cchat.ServersContainer) error {
|
func (g *Guild) Servers(container cchat.ServersContainer) error {
|
||||||
c, err := g.state.Channels(g.id)
|
c, err := g.state.Channels(g.id)
|
||||||
|
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat-discord/internal/segments"
|
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/segments/mention"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/segments/segutil"
|
||||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
)
|
)
|
||||||
|
@ -22,12 +24,12 @@ func NewUser(u discord.User, s *state.Instance) Author {
|
||||||
if u.Bot {
|
if u.Bot {
|
||||||
name.Content += " "
|
name.Content += " "
|
||||||
name.Segments = append(name.Segments,
|
name.Segments = append(name.Segments,
|
||||||
segments.NewBlurpleSegment(segments.Write(&name, "[BOT]")),
|
colored.NewBlurple(segutil.Write(&name, "[BOT]")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append a clickable user popup.
|
// Append a clickable user popup.
|
||||||
useg := segments.UserSegment(0, len(name.Content), u)
|
useg := mention.UserSegment(0, len(name.Content), u)
|
||||||
useg.WithState(s.State)
|
useg.WithState(s.State)
|
||||||
name.Segments = append(name.Segments, useg)
|
name.Segments = append(name.Segments, useg)
|
||||||
|
|
||||||
|
@ -59,7 +61,7 @@ func RenderMemberName(m discord.Member, g discord.Guild, s *state.Instance) text
|
||||||
// Update the color.
|
// Update the color.
|
||||||
if c := discord.MemberColor(g, m); c > 0 {
|
if c := discord.MemberColor(g, m); c > 0 {
|
||||||
name.Segments = append(name.Segments,
|
name.Segments = append(name.Segments,
|
||||||
segments.NewColored(len(name.Content), c.Uint32()),
|
colored.New(len(name.Content), c.Uint32()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,12 +69,12 @@ func RenderMemberName(m discord.Member, g discord.Guild, s *state.Instance) text
|
||||||
if m.User.Bot {
|
if m.User.Bot {
|
||||||
name.Content += " "
|
name.Content += " "
|
||||||
name.Segments = append(name.Segments,
|
name.Segments = append(name.Segments,
|
||||||
segments.NewBlurpleSegment(segments.Write(&name, "[BOT]")),
|
colored.NewBlurple(segutil.Write(&name, "[BOT]")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append a clickable user popup.
|
// Append a clickable user popup.
|
||||||
useg := segments.MemberSegment(0, len(name.Content), g, m)
|
useg := mention.MemberSegment(0, len(name.Content), g, m)
|
||||||
useg.WithState(s.State)
|
useg.WithState(s.State)
|
||||||
name.Segments = append(name.Segments, useg)
|
name.Segments = append(name.Segments, useg)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat-discord/internal/segments"
|
"github.com/diamondburned/cchat-discord/internal/segments"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/segments/mention"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -144,21 +145,21 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
|
||||||
// Request members in mentions if we're in a guild.
|
// Request members in mentions if we're in a guild.
|
||||||
if m.GuildID.IsValid() {
|
if m.GuildID.IsValid() {
|
||||||
for _, segment := range content.Segments {
|
for _, segment := range content.Segments {
|
||||||
if mention, ok := segment.(*segments.MentionSegment); ok {
|
if mention, ok := segment.(*mention.Segment); ok {
|
||||||
// If this is not a user mention, then skip.
|
// If this is not a user mention, then skip.
|
||||||
if mention.GuildUser == nil {
|
if mention.User == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we already have a member, then skip. We could check this
|
// If we already have a member, then skip. We could check this
|
||||||
// using the timestamp, as we might have a user set into the
|
// using the timestamp, as we might have a user set into the
|
||||||
// member field
|
// member field
|
||||||
if m := mention.GuildUser.Member; m != nil && m.Joined.IsValid() {
|
if mention.User.Member.Joined.IsValid() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request the member.
|
// Request the member.
|
||||||
s.MemberState.RequestMember(m.GuildID, mention.GuildUser.ID)
|
s.MemberState.RequestMember(m.GuildID, mention.User.Member.User.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Restorer cchat.SessionRestorer = restorer{}
|
||||||
|
|
||||||
|
type restorer struct{}
|
||||||
|
|
||||||
|
func (restorer) RestoreSession(data map[string]string) (cchat.Session, error) {
|
||||||
|
i, err := state.NewFromData(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewFromInstance(i)
|
||||||
|
}
|
|
@ -11,27 +11,20 @@ import (
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
"github.com/diamondburned/ningen"
|
"github.com/diamondburned/ningen"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrMFA = session.ErrMFA
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
*empty.Session
|
||||||
*state.Instance
|
*state.Instance
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func NewFromInstance(i *state.Instance) (cchat.Session, error) {
|
||||||
_ cchat.Iconer = (*Session)(nil)
|
return &Session{Instance: i}, 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 {
|
func (s *Session) ID() cchat.ID {
|
||||||
|
@ -48,8 +41,7 @@ func (s *Session) Name() text.Rich {
|
||||||
return text.Rich{Content: u.Username + "#" + u.Discriminator}
|
return text.Rich{Content: u.Username + "#" + u.Discriminator}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsIconer returns true.
|
func (s *Session) AsIconer() cchat.Iconer { return s }
|
||||||
func (s *Session) IsIconer() bool { return true }
|
|
||||||
|
|
||||||
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||||
u, err := s.Me()
|
u, err := s.Me()
|
||||||
|
@ -72,14 +64,7 @@ func (s *Session) Disconnect() error {
|
||||||
return s.Close()
|
return s.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSessionSaver returns true.
|
func (s *Session) AsSessionSaver() cchat.SessionSaver { return s.Instance }
|
||||||
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 {
|
func (s *Session) Servers(container cchat.ServersContainer) error {
|
||||||
// Reset the entire container when the session is closed.
|
// Reset the entire container when the session is closed.
|
||||||
|
|
|
@ -6,8 +6,10 @@ import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
"github.com/diamondburned/arikawa/session"
|
||||||
"github.com/diamondburned/arikawa/state"
|
"github.com/diamondburned/arikawa/state"
|
||||||
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
|
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/ningen"
|
"github.com/diamondburned/ningen"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -17,6 +19,22 @@ type Instance struct {
|
||||||
UserID discord.UserID
|
UserID discord.UserID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ cchat.SessionSaver = (*Instance)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
||||||
|
var ErrInvalidSession = errors.New("invalid session")
|
||||||
|
|
||||||
|
func NewFromData(data map[string]string) (*Instance, error) {
|
||||||
|
tk, ok := data["token"]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrInvalidSession
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewFromToken(tk)
|
||||||
|
}
|
||||||
|
|
||||||
func NewFromToken(token string) (*Instance, error) {
|
func NewFromToken(token string) (*Instance, error) {
|
||||||
s, err := state.New(token)
|
s, err := state.New(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -26,6 +44,16 @@ func NewFromToken(token string) (*Instance, error) {
|
||||||
return New(s)
|
return New(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Login(email, password, mfa string) (*Instance, error) {
|
||||||
|
session, err := session.Login(email, password, mfa)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
state, _ := state.NewFromSession(session, state.NewDefaultStore(nil))
|
||||||
|
return New(state)
|
||||||
|
}
|
||||||
|
|
||||||
func New(s *state.State) (*Instance, error) {
|
func New(s *state.State) (*Instance, error) {
|
||||||
// Prefetch user.
|
// Prefetch user.
|
||||||
u, err := s.Me()
|
u, err := s.Me()
|
||||||
|
@ -60,3 +88,9 @@ func (s *Instance) StateOnly() *state.State {
|
||||||
|
|
||||||
return s.WithContext(ctx)
|
return s.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Instance) SaveSession() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"token": s.Token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ func UserSegment(start, end int, u discord.User) NameSegment {
|
||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
um: User{
|
um: User{
|
||||||
member: discord.Member{User: u},
|
Member: discord.Member{User: u},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,8 @@ func MemberSegment(start, end int, guild discord.Guild, m discord.Member) NameSe
|
||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
um: User{
|
um: User{
|
||||||
guild: guild,
|
Guild: guild,
|
||||||
member: m,
|
Member: m,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,17 +56,17 @@ func (m NameSegment) Bounds() (start, end int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m NameSegment) AsMentioner() text.Mentioner {
|
func (m NameSegment) AsMentioner() text.Mentioner {
|
||||||
return m.um
|
return &m.um
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m NameSegment) AsAvatarer() text.Avatarer {
|
func (m NameSegment) AsAvatarer() text.Avatarer {
|
||||||
return m.um
|
return &m.um
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
state state.Store
|
state state.Store
|
||||||
guild discord.Guild
|
Guild discord.Guild
|
||||||
member discord.Member
|
Member discord.Member
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -97,18 +97,18 @@ func NewUser(state state.Store, guild discord.GuildID, guser discord.GuildUser)
|
||||||
|
|
||||||
return &User{
|
return &User{
|
||||||
state: state,
|
state: state,
|
||||||
guild: *g,
|
Guild: *g,
|
||||||
member: *guser.Member,
|
Member: *guser.Member,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *User) Color() uint32 {
|
func (um *User) Color() uint32 {
|
||||||
g, err := um.state.Guild(um.guild.ID)
|
g, err := um.state.Guild(um.Guild.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return colored.Blurple
|
return colored.Blurple
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.SolidColor(discord.MemberColor(*g, um.member).Uint32())
|
return text.SolidColor(discord.MemberColor(*g, um.Member).Uint32())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *User) AvatarSize() int {
|
func (um *User) AvatarSize() int {
|
||||||
|
@ -116,14 +116,14 @@ func (um *User) AvatarSize() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *User) AvatarText() string {
|
func (um *User) AvatarText() string {
|
||||||
if um.member.Nick != "" {
|
if um.Member.Nick != "" {
|
||||||
return um.member.Nick
|
return um.Member.Nick
|
||||||
}
|
}
|
||||||
return um.member.User.Username
|
return um.Member.User.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *User) Avatar() (url string) {
|
func (um *User) Avatar() (url string) {
|
||||||
return um.member.User.AvatarURL()
|
return um.Member.User.AvatarURL()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *User) MentionInfo() text.Rich {
|
func (um *User) MentionInfo() text.Rich {
|
||||||
|
@ -131,22 +131,22 @@ func (um *User) MentionInfo() text.Rich {
|
||||||
var segment text.Rich
|
var segment text.Rich
|
||||||
|
|
||||||
// Write the username if the user has a nickname.
|
// Write the username if the user has a nickname.
|
||||||
if um.member.Nick != "" {
|
if um.Member.Nick != "" {
|
||||||
content.WriteString("Username: ")
|
content.WriteString("Username: ")
|
||||||
content.WriteString(um.member.User.Username)
|
content.WriteString(um.Member.User.Username)
|
||||||
content.WriteByte('#')
|
content.WriteByte('#')
|
||||||
content.WriteString(um.member.User.Discriminator)
|
content.WriteString(um.Member.User.Discriminator)
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write extra information if any, but only if we have the guild state.
|
// Write extra information if any, but only if we have the guild state.
|
||||||
if len(um.member.RoleIDs) > 0 && um.guild.ID.IsValid() {
|
if len(um.Member.RoleIDs) > 0 && um.Guild.ID.IsValid() {
|
||||||
// Write a prepended new line, as role writes will always prepend a new
|
// Write a prepended new line, as role writes will always prepend a new
|
||||||
// line. This is to prevent a trailing new line.
|
// line. This is to prevent a trailing new line.
|
||||||
formatSectionf(&segment, &content, "Roles")
|
formatSectionf(&segment, &content, "Roles")
|
||||||
|
|
||||||
for _, id := range um.member.RoleIDs {
|
for _, id := range um.Member.RoleIDs {
|
||||||
rl, ok := findRole(um.guild.Roles, id)
|
rl, ok := findRole(um.Guild.Roles, id)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -169,19 +169,19 @@ func (um *User) MentionInfo() text.Rich {
|
||||||
// if the state is given.
|
// if the state is given.
|
||||||
if ningenState, ok := um.state.(*ningen.State); ok {
|
if ningenState, ok := um.state.(*ningen.State); ok {
|
||||||
// Does the user have rich presence? If so, write.
|
// Does the user have rich presence? If so, write.
|
||||||
if p, err := um.state.Presence(um.guild.ID, um.member.User.ID); err == nil {
|
if p, err := um.state.Presence(um.Guild.ID, um.Member.User.ID); err == nil {
|
||||||
for _, ac := range p.Activities {
|
for _, ac := range p.Activities {
|
||||||
formatActivity(&segment, &content, ac)
|
formatActivity(&segment, &content, ac)
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
} else if um.guild.ID.IsValid() {
|
} else if um.Guild.ID.IsValid() {
|
||||||
// If we're still in a guild, then we can ask Discord for that
|
// If we're still in a guild, then we can ask Discord for that
|
||||||
// member with their presence attached.
|
// member with their presence attached.
|
||||||
ningenState.MemberState.RequestMember(um.guild.ID, um.member.User.ID)
|
ningenState.MemberState.RequestMember(um.Guild.ID, um.Member.User.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the user's note if any.
|
// Write the user's note if any.
|
||||||
if note := ningenState.NoteState.Note(um.member.User.ID); note != "" {
|
if note := ningenState.NoteState.Note(um.Member.User.ID); note != "" {
|
||||||
formatSectionf(&segment, &content, "Note")
|
formatSectionf(&segment, &content, "Note")
|
||||||
content.WriteRune('\n')
|
content.WriteRune('\n')
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,13 @@ import (
|
||||||
|
|
||||||
// helper global functions
|
// helper global functions
|
||||||
|
|
||||||
|
func Write(rich *text.Rich, content string, segs ...text.Segment) (start, end int) {
|
||||||
|
start = len(rich.Content)
|
||||||
|
end = len(rich.Content) + len(content)
|
||||||
|
rich.Content += content
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func WriteBuf(w *bytes.Buffer, b []byte) (start, end int) {
|
func WriteBuf(w *bytes.Buffer, b []byte) (start, end int) {
|
||||||
start = w.Len()
|
start = w.Len()
|
||||||
w.Write(b)
|
w.Write(b)
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package discord
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat"
|
||||||
|
)
|
||||||
|
|
||||||
|
const LogoURL = "https://raw.githubusercontent.com/" +
|
||||||
|
"diamondburned/cchat-discord/himearikawa/discord_logo.png"
|
||||||
|
|
||||||
|
// Logo implements cchat.Iconer for the Discord logo.
|
||||||
|
var Logo cchat.Iconer = logo{}
|
||||||
|
|
||||||
|
type logo struct{}
|
||||||
|
|
||||||
|
func (logo) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||||
|
iconer.SetIcon(LogoURL)
|
||||||
|
return func() {}, nil
|
||||||
|
}
|
Loading…
Reference in New Issue