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
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/authenticate"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/session"
|
||||
"github.com/diamondburned/cchat/services"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
)
|
||||
|
||||
var service cchat.Service = Service{}
|
||||
|
||||
func init() {
|
||||
services.RegisterService(&Service{})
|
||||
services.RegisterService(service)
|
||||
}
|
||||
|
||||
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
||||
var ErrInvalidSession = errors.New("invalid session")
|
||||
|
||||
type Service struct{}
|
||||
|
||||
var (
|
||||
_ cchat.Iconer = (*Service)(nil)
|
||||
_ cchat.Service = (*Service)(nil)
|
||||
)
|
||||
type Service struct {
|
||||
empty.Service
|
||||
}
|
||||
|
||||
func (Service) Name() text.Rich {
|
||||
return text.Rich{Content: "Discord"}
|
||||
}
|
||||
|
||||
// IsIconer returns true.
|
||||
func (Service) IsIconer() bool { return true }
|
||||
|
||||
func (Service) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||
iconer.SetIcon("https://raw.githubusercontent.com/" +
|
||||
"diamondburned/cchat-discord/himearikawa/discord_logo.png")
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (Service) Authenticate() cchat.Authenticator {
|
||||
return &Authenticator{}
|
||||
return authenticate.New()
|
||||
}
|
||||
|
||||
func (s Service) RestoreSession(data map[string]string) (cchat.Session, error) {
|
||||
tk, ok := data["token"]
|
||||
if !ok {
|
||||
return nil, ErrInvalidSession
|
||||
}
|
||||
|
||||
return session.NewFromToken(tk)
|
||||
func (Service) AsIconer() cchat.Iconer {
|
||||
return Logo
|
||||
}
|
||||
|
||||
type Authenticator struct{}
|
||||
|
||||
var _ cchat.Authenticator = (*Authenticator)(nil)
|
||||
|
||||
func (*Authenticator) AuthenticateForm() []cchat.AuthenticateEntry {
|
||||
// TODO: username, password and 2FA
|
||||
return []cchat.AuthenticateEntry{
|
||||
{
|
||||
Name: "Token",
|
||||
Secret: true,
|
||||
},
|
||||
{
|
||||
Name: "(or) Username",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (*Authenticator) Authenticate(form []string) (cchat.Session, error) {
|
||||
switch {
|
||||
case form[0] != "": // Token
|
||||
return session.NewFromToken(form[0])
|
||||
case form[1] != "": // Username
|
||||
return nil, errors.New("username sign-in is not supported yet")
|
||||
}
|
||||
|
||||
return nil, errors.New("malformed authentication form")
|
||||
func (Service) AsSessionRestorer() cchat.SessionRestorer {
|
||||
return session.Restorer
|
||||
}
|
||||
|
|
|
@ -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/state"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -57,17 +58,13 @@ func FilterCategory(chs []discord.Channel, catID discord.ChannelID) []discord.Ch
|
|||
}
|
||||
|
||||
type Category struct {
|
||||
empty.Server
|
||||
id discord.ChannelID
|
||||
guildID discord.GuildID
|
||||
state *state.Instance
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.Server = (*Category)(nil)
|
||||
_ cchat.Lister = (*Category)(nil)
|
||||
)
|
||||
|
||||
func New(s *state.Instance, ch discord.Channel) *Category {
|
||||
func New(s *state.Instance, ch discord.Channel) cchat.Server {
|
||||
return &Category{
|
||||
id: ch.ID,
|
||||
guildID: ch.GuildID,
|
||||
|
@ -91,9 +88,7 @@ func (c *Category) Name() text.Rich {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Category) IsLister() bool {
|
||||
return true
|
||||
}
|
||||
func (c *Category) AsLister() cchat.Lister { return c }
|
||||
|
||||
func (c *Category) Servers(container cchat.ServersContainer) error {
|
||||
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 (
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"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/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
id discord.ChannelID
|
||||
guildID discord.GuildID
|
||||
state *state.Instance
|
||||
*empty.Server
|
||||
*shared.Channel
|
||||
}
|
||||
|
||||
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 &Channel{
|
||||
id: ch.ID,
|
||||
guildID: ch.GuildID,
|
||||
state: s,
|
||||
return Channel{
|
||||
Channel: &shared.Channel{
|
||||
ID: ch.ID,
|
||||
GuildID: ch.GuildID,
|
||||
State: s,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// self does not do IO.
|
||||
func (ch *Channel) self() (*discord.Channel, error) {
|
||||
return ch.state.Store.Channel(ch.id)
|
||||
func (ch Channel) self() (*discord.Channel, error) {
|
||||
return ch.State.Store.Channel(ch.Channel.ID)
|
||||
}
|
||||
|
||||
// messages does not do IO.
|
||||
func (ch *Channel) messages() ([]discord.Message, error) {
|
||||
return ch.state.Store.Messages(ch.id)
|
||||
func (ch Channel) messages() ([]discord.Message, error) {
|
||||
return ch.State.Store.Messages(ch.Channel.ID)
|
||||
}
|
||||
|
||||
func (ch *Channel) guild() (*discord.Guild, error) {
|
||||
if ch.guildID.IsValid() {
|
||||
return ch.state.Store.Guild(ch.guildID)
|
||||
func (ch Channel) guild() (*discord.Guild, error) {
|
||||
if ch.GuildID.IsValid() {
|
||||
return ch.State.Store.Guild(ch.GuildID)
|
||||
}
|
||||
return nil, errors.New("channel not in a guild")
|
||||
}
|
||||
|
||||
func (ch *Channel) ID() cchat.ID {
|
||||
return ch.id.String()
|
||||
func (ch Channel) ID() cchat.ID {
|
||||
return ch.Channel.ID.String()
|
||||
}
|
||||
|
||||
func (ch *Channel) Name() text.Rich {
|
||||
func (ch Channel) Name() text.Rich {
|
||||
c, err := ch.self()
|
||||
if err != nil {
|
||||
return text.Rich{Content: ch.id.String()}
|
||||
return text.Rich{Content: ch.Channel.ID.String()}
|
||||
}
|
||||
|
||||
if c.NSFW {
|
||||
|
@ -63,3 +67,11 @@ func (ch *Channel) Name() text.Rich {
|
|||
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 (
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ cchat.Actioner = (*Channel)(nil)
|
||||
type Actioner struct {
|
||||
*shared.Channel
|
||||
}
|
||||
|
||||
// IsActioner returns true.
|
||||
func (ch *Channel) IsActioner() bool { return true }
|
||||
var _ cchat.Actioner = (*Actioner)(nil)
|
||||
|
||||
func New(ch *shared.Channel) Actioner {
|
||||
return Actioner{ch}
|
||||
}
|
||||
|
||||
const (
|
||||
ActionDelete = "Delete"
|
||||
|
@ -17,7 +23,7 @@ const (
|
|||
|
||||
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)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse ID")
|
||||
|
@ -25,25 +31,25 @@ func (ch *Channel) DoMessageAction(action, id string) error {
|
|||
|
||||
switch action {
|
||||
case ActionDelete:
|
||||
return ch.state.DeleteMessage(ch.id, discord.MessageID(s))
|
||||
return ac.State.DeleteMessage(ac.ID, discord.MessageID(s))
|
||||
default:
|
||||
return ErrUnknownAction
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) MessageActions(id string) []string {
|
||||
func (ac Actioner) Actions(id string) []string {
|
||||
s, err := discord.ParseSnowflake(id)
|
||||
if err != 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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the current user.
|
||||
u, err := ch.state.Store.Me()
|
||||
u, err := ac.State.Store.Me()
|
||||
if err != 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
|
||||
// us to delete others' messages.
|
||||
if !canDelete {
|
||||
canDelete = ch.canManageMessages(u.ID)
|
||||
canDelete = ac.canManageMessages(u.ID)
|
||||
}
|
||||
|
||||
if canDelete {
|
||||
|
@ -66,26 +72,26 @@ func (ch *Channel) MessageActions(id string) []string {
|
|||
|
||||
// canManageMessages returns whether or not the user is allowed to manage
|
||||
// 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 !ch.guildID.IsValid() {
|
||||
if !ac.GuildID.IsValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
// We need the guild, member and channel to calculate the permission
|
||||
// overrides.
|
||||
|
||||
g, err := ch.guild()
|
||||
g, err := ac.Guild()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
c, err := ch.self()
|
||||
c, err := ac.Self()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
m, err := ch.state.Store.Member(ch.guildID, userID)
|
||||
m, err := ac.State.Store.Member(ac.GuildID, userID)
|
||||
if err != nil {
|
||||
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 (
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||
"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 (ch *Channel) IsEditor() bool {
|
||||
p, err := ch.state.StateOnly().Permissions(ch.id, ch.state.UserID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.Has(discord.PermissionSendMessages)
|
||||
func New(ch *shared.Channel) cchat.Editor {
|
||||
return Editor{ch}
|
||||
}
|
||||
|
||||
// MessageEditable returns true if the given message ID belongs to the current
|
||||
// user.
|
||||
func (ch *Channel) MessageEditable(id string) bool {
|
||||
func (ed Editor) MessageEditable(id string) bool {
|
||||
s, err := discord.ParseSnowflake(id)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
m, err := ch.state.Store.Message(ch.id, discord.MessageID(s))
|
||||
m, err := ed.State.Store.Message(ed.ID, discord.MessageID(s))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.Author.ID == ch.state.UserID
|
||||
return m.Author.ID == ed.State.UserID
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
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 {
|
||||
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.
|
||||
func (ch *Channel) EditMessage(id, content string) error {
|
||||
func (ed Editor) EditMessage(id, content string) error {
|
||||
s, err := discord.ParseSnowflake(id)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to parse ID")
|
||||
}
|
||||
|
||||
_, err = ch.state.EditText(ch.id, discord.MessageID(s), content)
|
||||
_, err = ed.State.EditText(ed.ID, discord.MessageID(s), content)
|
||||
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/gateway"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||
"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/text"
|
||||
)
|
||||
|
||||
type Member struct {
|
||||
Channel
|
||||
state *state.Instance
|
||||
|
||||
channel *shared.Channel
|
||||
userID discord.UserID
|
||||
origName string // use if cache is stale
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.ListMember = (*Member)(nil)
|
||||
_ cchat.Iconer = (*Member)(nil)
|
||||
)
|
||||
|
||||
// New creates a new list member. it.Member must not be nil.
|
||||
func (c Channel) NewMember(opItem gateway.GuildMemberListOpItem) *Member {
|
||||
func NewMember(ch *shared.Channel, opItem gateway.GuildMemberListOpItem) cchat.ListMember {
|
||||
return &Member{
|
||||
Channel: c,
|
||||
channel: ch,
|
||||
userID: opItem.Member.User.ID,
|
||||
origName: opItem.Member.User.Username,
|
||||
}
|
||||
|
@ -41,12 +36,12 @@ func (l *Member) ID() cchat.ID {
|
|||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return text.Plain(l.origName)
|
||||
}
|
||||
|
@ -56,8 +51,8 @@ func (l *Member) Name() text.Rich {
|
|||
name = m.Nick
|
||||
}
|
||||
|
||||
mention := segments.MemberSegment(0, len(name), *g, *m)
|
||||
mention.WithState(l.state.State)
|
||||
mention := mention.MemberSegment(0, len(name), *g, *m)
|
||||
mention.WithState(l.channel.State.State)
|
||||
|
||||
var txt = text.Rich{
|
||||
Content: name,
|
||||
|
@ -65,17 +60,16 @@ func (l *Member) Name() text.Rich {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// IsIconer returns true.
|
||||
func (l *Member) IsIconer() bool { return true }
|
||||
func (l *Member) AsIconer() cchat.Iconer { return l }
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -85,28 +79,28 @@ func (l *Member) Icon(ctx context.Context, c cchat.IconContainer) (func(), error
|
|||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (l *Member) Status() cchat.UserStatus {
|
||||
p, err := l.state.Store.Presence(l.guildID, l.userID)
|
||||
func (l *Member) Status() cchat.Status {
|
||||
p, err := l.channel.State.Store.Presence(l.channel.GuildID, l.userID)
|
||||
if err != nil {
|
||||
return cchat.UnknownStatus
|
||||
return cchat.StatusUnknown
|
||||
}
|
||||
|
||||
switch p.Status {
|
||||
case discord.OnlineStatus:
|
||||
return cchat.OnlineStatus
|
||||
return cchat.StatusOnline
|
||||
case discord.DoNotDisturbStatus:
|
||||
return cchat.BusyStatus
|
||||
return cchat.StatusBusy
|
||||
case discord.IdleStatus:
|
||||
return cchat.AwayStatus
|
||||
return cchat.StatusAway
|
||||
case discord.OfflineStatus, discord.InvisibleStatus:
|
||||
return cchat.OfflineStatus
|
||||
return cchat.StatusOffline
|
||||
default:
|
||||
return cchat.UnknownStatus
|
||||
return cchat.StatusUnknown
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return text.Plain("")
|
||||
}
|
||||
|
@ -142,11 +136,9 @@ func formatSmallActivity(ac discord.Activity) text.Rich {
|
|||
status.WriteString(ac.Emoji.Name)
|
||||
status.WriteByte(' ')
|
||||
} else {
|
||||
segmts = append(segmts, segments.EmojiSegment{
|
||||
Start: status.Len(),
|
||||
Name: ac.Emoji.Name,
|
||||
EmojiURL: ac.Emoji.EmojiURL() + "?size=64",
|
||||
Large: ac.State == "",
|
||||
segmts = append(segmts, emoji.Segment{
|
||||
Start: status.Len(),
|
||||
Emoji: emoji.EmojiFromDiscord(*ac.Emoji, ac.State == ""),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
package channel
|
||||
package memberlist
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -30,24 +30,21 @@ func seekPrevGroup(l *member.List, ix int) (item, group gateway.GuildMemberListO
|
|||
return
|
||||
}
|
||||
|
||||
var _ cchat.MemberLister = (*Channel)(nil)
|
||||
|
||||
// IsMemberLister returns true if the channel is a guild channel.
|
||||
func (ch *Channel) IsMemberLister() bool {
|
||||
return ch.guildID.IsValid()
|
||||
type MemberLister struct {
|
||||
*shared.Channel
|
||||
}
|
||||
|
||||
func (ch *Channel) memberListCh() memberlist.Channel {
|
||||
return memberlist.NewChannel(ch.state, ch.id, ch.guildID)
|
||||
func New(ch *shared.Channel) cchat.MemberLister {
|
||||
return MemberLister{ch}
|
||||
}
|
||||
|
||||
func (ch *Channel) ListMembers(ctx context.Context, c cchat.MemberListContainer) (func(), error) {
|
||||
if !ch.guildID.IsValid() {
|
||||
func (ml MemberLister) ListMembers(ctx context.Context, c cchat.MemberListContainer) (func(), error) {
|
||||
if !ml.GuildID.IsValid() {
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
cancel := ch.state.AddHandler(func(u *gateway.GuildMemberListUpdate) {
|
||||
l, err := ch.state.MemberState.GetMemberList(ch.guildID, ch.id)
|
||||
cancel := ml.State.AddHandler(func(u *gateway.GuildMemberListUpdate) {
|
||||
l, err := ml.State.MemberState.GetMemberList(ml.GuildID, ml.ID)
|
||||
if err != nil {
|
||||
return // wat
|
||||
}
|
||||
|
@ -56,44 +53,41 @@ func (ch *Channel) ListMembers(ctx context.Context, c cchat.MemberListContainer)
|
|||
return
|
||||
}
|
||||
|
||||
var listCh = ch.memberListCh()
|
||||
|
||||
for _, ev := range u.Ops {
|
||||
switch ev.Op {
|
||||
case "SYNC":
|
||||
ch.checkSync(c)
|
||||
ml.checkSync(c)
|
||||
|
||||
case "INSERT", "UPDATE":
|
||||
item, group := seekPrevGroup(l, ev.Index)
|
||||
if item.Member != nil && group.Group != nil {
|
||||
c.SetMember(group.Group.ID, listCh.NewMember(item))
|
||||
listCh.FlushMemberGroups(l, c)
|
||||
c.SetMember(group.Group.ID, NewMember(ml.Channel, item))
|
||||
ml.FlushMemberGroups(l, c)
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
_, group := seekPrevGroup(l, ev.Index-1)
|
||||
if group.Group != nil && ev.Item.Member != nil {
|
||||
c.RemoveMember(group.Group.ID, ev.Item.Member.User.ID.String())
|
||||
listCh.FlushMemberGroups(l, c)
|
||||
ml.FlushMemberGroups(l, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ch.checkSync(c)
|
||||
ml.checkSync(c)
|
||||
|
||||
return cancel, nil
|
||||
}
|
||||
|
||||
func (ch *Channel) checkSync(c cchat.MemberListContainer) {
|
||||
l, err := ch.state.MemberState.GetMemberList(ch.guildID, ch.id)
|
||||
func (ml MemberLister) checkSync(c cchat.MemberListContainer) {
|
||||
l, err := ml.State.MemberState.GetMemberList(ml.GuildID, ml.ID)
|
||||
if err != nil {
|
||||
ch.state.MemberState.RequestMemberList(ch.guildID, ch.id, 0)
|
||||
ml.State.MemberState.RequestMemberList(ml.GuildID, ml.ID, 0)
|
||||
return
|
||||
}
|
||||
|
||||
listCh := ch.memberListCh()
|
||||
listCh.FlushMemberGroups(l, c)
|
||||
ml.FlushMemberGroups(l, c)
|
||||
|
||||
l.ViewItems(func(items []gateway.GuildMemberListOpItem) {
|
||||
var group gateway.GuildMemberListGroup
|
||||
|
@ -104,8 +98,19 @@ func (ch *Channel) checkSync(c cchat.MemberListContainer) {
|
|||
group = *item.Group
|
||||
|
||||
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 (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"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/pkg/errors"
|
||||
)
|
||||
|
||||
var _ cchat.Nicknamer = (*Channel)(nil)
|
||||
|
||||
// IsNicknamer returns true if the current channel is in a guild.
|
||||
func (ch *Channel) IsNicknamer() bool {
|
||||
return ch.guildID.IsValid()
|
||||
type Nicknamer struct {
|
||||
*shared.Channel
|
||||
}
|
||||
|
||||
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.
|
||||
if !ch.guildID.IsValid() {
|
||||
if !nn.GuildID.IsValid() {
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
state := ch.state.WithContext(ctx)
|
||||
state := nn.State.WithContext(ctx)
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
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 {
|
||||
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.
|
||||
var selfID = m.User.ID
|
||||
|
||||
return ch.state.AddHandler(func(g *gateway.GuildMemberUpdateEvent) {
|
||||
if g.GuildID != ch.guildID || g.User.ID != selfID {
|
||||
return nn.State.AddHandler(func(g *gateway.GuildMemberUpdateEvent) {
|
||||
if g.GuildID != nn.GuildID || g.User.ID != selfID {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -61,10 +63,10 @@ func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) (
|
|||
rich.Content = m.Nick
|
||||
}
|
||||
|
||||
c, err := ch.state.MemberColor(g.GuildID, selfID)
|
||||
c, err := nn.State.MemberColor(g.GuildID, selfID)
|
||||
if err == nil {
|
||||
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 (
|
||||
"strings"
|
||||
|
||||
"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/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
type Completer struct {
|
||||
*shared.Channel
|
||||
}
|
||||
|
||||
const MaxCompletion = 15
|
||||
|
||||
var _ cchat.MessageCompleter = (*Channel)(nil)
|
||||
|
||||
// IsMessageCompleter returns true if the user can send messages in this
|
||||
// channel.
|
||||
func (ch *Channel) IsMessageCompleter() bool {
|
||||
p, err := ch.state.StateOnly().Permissions(ch.id, ch.state.UserID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.Has(discord.PermissionSendMessages)
|
||||
func New(ch *shared.Channel) cchat.Completer {
|
||||
return Completer{ch}
|
||||
}
|
||||
|
||||
// CompleteMessage implements message input completion capability for Discord.
|
||||
// This method supports user mentions, channel mentions and emojis.
|
||||
//
|
||||
// For the individual implementations, refer to channel_completion.go.
|
||||
func (ch *Channel) CompleteMessage(words []string, i int) (entries []cchat.CompletionEntry) {
|
||||
func (ch Completer) Complete(words []string, i int64) (entries []cchat.CompletionEntry) {
|
||||
var word = words[i]
|
||||
// Word should have at least a character for the char check.
|
||||
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 word == "" {
|
||||
msgs, _ := ch.messages()
|
||||
g, _ := ch.guild() // nil is fine
|
||||
msgs, _ := ch.State.Store.Messages(ch.ID)
|
||||
g, _ := ch.State.Store.Guild(ch.GuildID) // nil is fine
|
||||
|
||||
// Keep track of the number of authors.
|
||||
// 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.
|
||||
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 {
|
||||
return
|
||||
|
@ -103,8 +99,8 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
|
|||
var match = strings.ToLower(word)
|
||||
|
||||
// If we're not in a guild, then we can check the list of recipients.
|
||||
if !ch.guildID.IsValid() {
|
||||
c, err := ch.self()
|
||||
if !ch.GuildID.IsValid() {
|
||||
c, err := ch.State.Store.Channel(ch.ID)
|
||||
if err != nil {
|
||||
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.
|
||||
m, merr := ch.state.Store.Members(ch.guildID)
|
||||
g, gerr := ch.guild()
|
||||
m, merr := ch.State.Store.Members(ch.GuildID)
|
||||
g, gerr := ch.State.Store.Guild(ch.GuildID)
|
||||
|
||||
if merr != nil || gerr != nil {
|
||||
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
|
||||
// search for them.
|
||||
if len(m) == 0 {
|
||||
ch.state.MemberState.SearchMember(ch.guildID, word)
|
||||
ch.State.MemberState.SearchMember(ch.GuildID, word)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -145,7 +141,7 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
|
|||
if contains(match, mem.User.Username, mem.Nick) {
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
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},
|
||||
IconURL: mem.User.AvatarURL(),
|
||||
})
|
||||
|
@ -158,18 +154,18 @@ func (ch *Channel) completeMentions(word string) (entries []cchat.CompletionEntr
|
|||
return
|
||||
}
|
||||
|
||||
func (ch *Channel) completeChannels(word string) (entries []cchat.CompletionEntry) {
|
||||
func (ch Completer) completeChannels(word string) (entries []cchat.CompletionEntry) {
|
||||
// Ignore if empty word.
|
||||
if word == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore if we're not in a guild.
|
||||
if !ch.guildID.IsValid() {
|
||||
if !ch.GuildID.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
c, err := ch.state.State.Channels(ch.guildID)
|
||||
c, err := ch.State.Store.Channels(ch.GuildID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -183,7 +179,7 @@ func (ch *Channel) completeChannels(word string) (entries []cchat.CompletionEntr
|
|||
|
||||
var category string
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -202,13 +198,13 @@ func (ch *Channel) completeChannels(word string) (entries []cchat.CompletionEntr
|
|||
return
|
||||
}
|
||||
|
||||
func (ch *Channel) completeEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||
func (ch Completer) completeEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||
// Ignore if empty word.
|
||||
if word == "" {
|
||||
return
|
||||
}
|
||||
|
||||
e, err := ch.state.EmojiState.Get(ch.guildID)
|
||||
e, err := ch.State.EmojiState.Get(ch.GuildID)
|
||||
if err != nil {
|
||||
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-discord/internal/discord/guild"
|
||||
"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/utils/empty"
|
||||
)
|
||||
|
||||
type GuildFolder struct {
|
||||
empty.Server
|
||||
gateway.GuildFolder
|
||||
state *state.Instance
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.Server = (*GuildFolder)(nil)
|
||||
_ cchat.Lister = (*GuildFolder)(nil)
|
||||
)
|
||||
|
||||
func New(s *state.Instance, gf gateway.GuildFolder) *GuildFolder {
|
||||
func New(s *state.Instance, gf gateway.GuildFolder) cchat.Server {
|
||||
// Name should never be empty.
|
||||
if gf.Name == "" {
|
||||
var names = make([]string, 0, len(gf.GuildIDs))
|
||||
|
@ -55,7 +52,7 @@ func (gf *GuildFolder) Name() text.Rich {
|
|||
if gf.GuildFolder.Color > 0 {
|
||||
name.Segments = []text.Segment{
|
||||
// The length of this black box is actually 3. Mind == blown.
|
||||
segments.NewColored(len(name.Content), gf.GuildFolder.Color.Uint32()),
|
||||
colored.New(len(name.Content), gf.GuildFolder.Color.Uint32()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +60,7 @@ func (gf *GuildFolder) Name() text.Rich {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
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/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Guild struct {
|
||||
empty.Server
|
||||
id discord.GuildID
|
||||
state *state.Instance
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.Iconer = (*Guild)(nil)
|
||||
_ cchat.Server = (*Guild)(nil)
|
||||
_ cchat.Lister = (*Guild)(nil)
|
||||
)
|
||||
|
||||
func New(s *state.Instance, g *discord.Guild) *Guild {
|
||||
func New(s *state.Instance, g *discord.Guild) cchat.Server {
|
||||
return &Guild{
|
||||
id: g.ID,
|
||||
state: s,
|
||||
}
|
||||
}
|
||||
|
||||
func NewFromID(s *state.Instance, gID discord.GuildID) (*Guild, error) {
|
||||
func NewFromID(s *state.Instance, gID discord.GuildID) (cchat.Server, error) {
|
||||
g, err := s.Guild(gID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -64,11 +60,7 @@ func (g *Guild) Name() text.Rich {
|
|||
return text.Rich{Content: s.Name}
|
||||
}
|
||||
|
||||
// IsIconer returns true if the guild has an icon.
|
||||
func (g *Guild) IsIconer() bool {
|
||||
s, err := g.selfState()
|
||||
return err == nil && s.Icon != ""
|
||||
}
|
||||
func (g *Guild) AsIconer() cchat.Iconer { return g }
|
||||
|
||||
func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||
s, err := g.self(ctx)
|
||||
|
@ -89,8 +81,7 @@ func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), e
|
|||
}), nil
|
||||
}
|
||||
|
||||
// IsLister returns true.
|
||||
func (g *Guild) IsLister() bool { return true }
|
||||
func (g *Guild) AsLister() cchat.Lister { return g }
|
||||
|
||||
func (g *Guild) Servers(container cchat.ServersContainer) error {
|
||||
c, err := g.state.Channels(g.id)
|
||||
|
|
|
@ -4,7 +4,9 @@ import (
|
|||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments"
|
||||
"github.com/diamondburned/cchat-discord/internal/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/text"
|
||||
)
|
||||
|
@ -22,12 +24,12 @@ func NewUser(u discord.User, s *state.Instance) Author {
|
|||
if u.Bot {
|
||||
name.Content += " "
|
||||
name.Segments = append(name.Segments,
|
||||
segments.NewBlurpleSegment(segments.Write(&name, "[BOT]")),
|
||||
colored.NewBlurple(segutil.Write(&name, "[BOT]")),
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
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.
|
||||
if c := discord.MemberColor(g, m); c > 0 {
|
||||
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 {
|
||||
name.Content += " "
|
||||
name.Segments = append(name.Segments,
|
||||
segments.NewBlurpleSegment(segments.Write(&name, "[BOT]")),
|
||||
colored.NewBlurple(segutil.Write(&name, "[BOT]")),
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
name.Segments = append(name.Segments, useg)
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/mention"
|
||||
"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.
|
||||
if m.GuildID.IsValid() {
|
||||
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 mention.GuildUser == nil {
|
||||
if mention.User == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we already have a member, then skip. We could check this
|
||||
// using the timestamp, as we might have a user set into the
|
||||
// member field
|
||||
if m := mention.GuildUser.Member; m != nil && m.Joined.IsValid() {
|
||||
if mention.User.Member.Joined.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/diamondburned/ningen"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrMFA = session.ErrMFA
|
||||
|
||||
type Session struct {
|
||||
*empty.Session
|
||||
*state.Instance
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.Iconer = (*Session)(nil)
|
||||
_ cchat.Session = (*Session)(nil)
|
||||
_ cchat.SessionSaver = (*Session)(nil)
|
||||
)
|
||||
|
||||
func NewFromToken(token string) (*Session, error) {
|
||||
i, err := state.NewFromToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Session{i}, nil
|
||||
func NewFromInstance(i *state.Instance) (cchat.Session, error) {
|
||||
return &Session{Instance: i}, nil
|
||||
}
|
||||
|
||||
func (s *Session) ID() cchat.ID {
|
||||
|
@ -48,8 +41,7 @@ func (s *Session) Name() text.Rich {
|
|||
return text.Rich{Content: u.Username + "#" + u.Discriminator}
|
||||
}
|
||||
|
||||
// IsIconer returns true.
|
||||
func (s *Session) IsIconer() bool { return true }
|
||||
func (s *Session) AsIconer() cchat.Iconer { return s }
|
||||
|
||||
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||
u, err := s.Me()
|
||||
|
@ -72,14 +64,7 @@ func (s *Session) Disconnect() error {
|
|||
return s.Close()
|
||||
}
|
||||
|
||||
// IsSessionSaver returns true.
|
||||
func (s *Session) IsSessionSaver() bool { return true }
|
||||
|
||||
func (s *Session) SaveSession() map[string]string {
|
||||
return map[string]string{
|
||||
"token": s.Token,
|
||||
}
|
||||
}
|
||||
func (s *Session) AsSessionSaver() cchat.SessionSaver { return s.Instance }
|
||||
|
||||
func (s *Session) Servers(container cchat.ServersContainer) error {
|
||||
// Reset the entire container when the session is closed.
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
"log"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/session"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/ningen"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
@ -17,6 +19,22 @@ type Instance struct {
|
|||
UserID discord.UserID
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.SessionSaver = (*Instance)(nil)
|
||||
)
|
||||
|
||||
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
||||
var ErrInvalidSession = errors.New("invalid session")
|
||||
|
||||
func NewFromData(data map[string]string) (*Instance, error) {
|
||||
tk, ok := data["token"]
|
||||
if !ok {
|
||||
return nil, ErrInvalidSession
|
||||
}
|
||||
|
||||
return NewFromToken(tk)
|
||||
}
|
||||
|
||||
func NewFromToken(token string) (*Instance, error) {
|
||||
s, err := state.New(token)
|
||||
if err != nil {
|
||||
|
@ -26,6 +44,16 @@ func NewFromToken(token string) (*Instance, error) {
|
|||
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) {
|
||||
// Prefetch user.
|
||||
u, err := s.Me()
|
||||
|
@ -60,3 +88,9 @@ func (s *Instance) StateOnly() *state.State {
|
|||
|
||||
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,
|
||||
end: end,
|
||||
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,
|
||||
end: end,
|
||||
um: User{
|
||||
guild: guild,
|
||||
member: m,
|
||||
Guild: guild,
|
||||
Member: m,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -56,17 +56,17 @@ func (m NameSegment) Bounds() (start, end int) {
|
|||
}
|
||||
|
||||
func (m NameSegment) AsMentioner() text.Mentioner {
|
||||
return m.um
|
||||
return &m.um
|
||||
}
|
||||
|
||||
func (m NameSegment) AsAvatarer() text.Avatarer {
|
||||
return m.um
|
||||
return &m.um
|
||||
}
|
||||
|
||||
type User struct {
|
||||
state state.Store
|
||||
guild discord.Guild
|
||||
member discord.Member
|
||||
Guild discord.Guild
|
||||
Member discord.Member
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -97,18 +97,18 @@ func NewUser(state state.Store, guild discord.GuildID, guser discord.GuildUser)
|
|||
|
||||
return &User{
|
||||
state: state,
|
||||
guild: *g,
|
||||
member: *guser.Member,
|
||||
Guild: *g,
|
||||
Member: *guser.Member,
|
||||
}
|
||||
}
|
||||
|
||||
func (um *User) Color() uint32 {
|
||||
g, err := um.state.Guild(um.guild.ID)
|
||||
g, err := um.state.Guild(um.Guild.ID)
|
||||
if err != nil {
|
||||
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 {
|
||||
|
@ -116,14 +116,14 @@ func (um *User) AvatarSize() int {
|
|||
}
|
||||
|
||||
func (um *User) AvatarText() string {
|
||||
if um.member.Nick != "" {
|
||||
return um.member.Nick
|
||||
if um.Member.Nick != "" {
|
||||
return um.Member.Nick
|
||||
}
|
||||
return um.member.User.Username
|
||||
return um.Member.User.Username
|
||||
}
|
||||
|
||||
func (um *User) Avatar() (url string) {
|
||||
return um.member.User.AvatarURL()
|
||||
return um.Member.User.AvatarURL()
|
||||
}
|
||||
|
||||
func (um *User) MentionInfo() text.Rich {
|
||||
|
@ -131,22 +131,22 @@ func (um *User) MentionInfo() text.Rich {
|
|||
var segment text.Rich
|
||||
|
||||
// Write the username if the user has a nickname.
|
||||
if um.member.Nick != "" {
|
||||
if um.Member.Nick != "" {
|
||||
content.WriteString("Username: ")
|
||||
content.WriteString(um.member.User.Username)
|
||||
content.WriteString(um.Member.User.Username)
|
||||
content.WriteByte('#')
|
||||
content.WriteString(um.member.User.Discriminator)
|
||||
content.WriteString(um.Member.User.Discriminator)
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// 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
|
||||
// line. This is to prevent a trailing new line.
|
||||
formatSectionf(&segment, &content, "Roles")
|
||||
|
||||
for _, id := range um.member.RoleIDs {
|
||||
rl, ok := findRole(um.guild.Roles, id)
|
||||
for _, id := range um.Member.RoleIDs {
|
||||
rl, ok := findRole(um.Guild.Roles, id)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
@ -169,19 +169,19 @@ func (um *User) MentionInfo() text.Rich {
|
|||
// if the state is given.
|
||||
if ningenState, ok := um.state.(*ningen.State); ok {
|
||||
// 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 {
|
||||
formatActivity(&segment, &content, ac)
|
||||
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
|
||||
// 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.
|
||||
if note := ningenState.NoteState.Note(um.member.User.ID); note != "" {
|
||||
if note := ningenState.NoteState.Note(um.Member.User.ID); note != "" {
|
||||
formatSectionf(&segment, &content, "Note")
|
||||
content.WriteRune('\n')
|
||||
|
||||
|
|
|
@ -8,6 +8,13 @@ import (
|
|||
|
||||
// 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) {
|
||||
start = w.Len()
|
||||
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