cchat-discord/service.go

234 lines
5.1 KiB
Go

package discord
import (
"context"
"log"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/arikawa/session"
"github.com/diamondburned/arikawa/state"
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat/services"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/ningen"
"github.com/pkg/errors"
)
func init() {
services.RegisterService(&Service{})
}
// ErrInvalidSession is returned if SessionRestore is given a bad session.
var ErrInvalidSession = errors.New("invalid session")
type Service struct{}
var (
_ cchat.Icon = (*Service)(nil)
_ cchat.Service = (*Service)(nil)
)
func (Service) Name() text.Rich {
return text.Rich{Content: "Discord"}
}
func (Service) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
iconer.SetIcon("https://raw.githubusercontent.com/" +
"diamondburned/cchat-discord/himearikawa/discord_logo.png")
return func() {}, nil
}
func (Service) Authenticate() cchat.Authenticator {
return Authenticator{}
}
func (s Service) RestoreSession(data map[string]string) (cchat.Session, error) {
tk, ok := data["token"]
if !ok {
return nil, ErrInvalidSession
}
return NewSessionToken(tk)
}
type Authenticator struct{}
var _ cchat.Authenticator = (*Authenticator)(nil)
func (Authenticator) AuthenticateForm() []cchat.AuthenticateEntry {
// TODO: username, password and 2FA
return []cchat.AuthenticateEntry{
{
Name: "Token",
Secret: true,
},
}
}
func (Authenticator) Authenticate(form []string) (cchat.Session, error) {
return NewSessionToken(form[0])
}
type Session struct {
*ningen.State
userID discord.UserID
}
var (
_ cchat.Icon = (*Session)(nil)
_ cchat.Session = (*Session)(nil)
_ cchat.SessionSaver = (*Session)(nil)
)
func NewSessionToken(token string) (*Session, error) {
s, err := state.New(token)
if err != nil {
return nil, err
}
return NewSession(s)
}
func NewSession(s *state.State) (*Session, error) {
// Prefetch user.
u, err := s.Me()
if err != nil {
return nil, errors.Wrap(err, "Failed to get current user")
}
n, err := ningen.FromState(s)
if err != nil {
return nil, errors.Wrap(err, "Failed to create a state wrapper")
}
n.Client.OnRequest = append(n.Client.OnRequest, func(r httpdriver.Request) error {
log.Println("[Discord] Request", r.GetPath())
return nil
})
if err := n.Open(); err != nil {
return nil, err
}
return &Session{
userID: u.ID,
State: n,
}, nil
}
func (s *Session) ID() cchat.ID {
return s.userID.String()
}
func (s *Session) Name() text.Rich {
u, err := s.Store.Me()
if err != nil {
// This shouldn't happen, ever.
return text.Rich{Content: "<@" + s.userID.String() + ">"}
}
return text.Rich{Content: u.Username + "#" + u.Discriminator}
}
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
u, err := s.Me()
if err != nil {
return nil, errors.Wrap(err, "Failed to get the current user")
}
// Thanks to arikawa, AvatarURL is never empty.
iconer.SetIcon(AvatarURL(u.AvatarURL()))
return s.AddHandler(func(*gateway.UserUpdateEvent) {
// Bypass the event and use the state cache.
if u, err := s.Store.Me(); err == nil {
iconer.SetIcon(AvatarURL(u.AvatarURL()))
}
}), nil
}
func (s *Session) Disconnect() error {
return s.Close()
}
func (s *Session) Save() (map[string]string, error) {
return map[string]string{
"token": s.Token,
}, nil
}
func (s *Session) Servers(container cchat.ServersContainer) error {
// Reset the entire container when the session is closed.
s.AddHandler(func(*session.Closed) {
container.SetServers(nil)
})
// Set the entire container again once reconnected.
s.AddHandler(func(*ningen.Connected) {
s.servers(container)
})
return s.servers(container)
}
func (s *Session) servers(container cchat.ServersContainer) error {
switch {
// If the user has guild folders:
case len(s.Ready.Settings.GuildFolders) > 0:
// TODO: account for missing guilds.
var toplevels = make([]cchat.Server, 0, len(s.Ready.Settings.GuildFolders))
for _, folder := range s.Ready.Settings.GuildFolders {
// TODO: correct.
switch {
case folder.ID > 0:
fallthrough
case len(folder.GuildIDs) > 1:
toplevels = append(toplevels, NewGuildFolder(s, folder))
case len(folder.GuildIDs) == 1:
g, err := NewGuildFromID(s, folder.GuildIDs[0])
if err != nil {
continue
}
toplevels = append(toplevels, g)
}
}
container.SetServers(toplevels)
// If the user doesn't have guild folders but has sorted their guilds
// before:
case len(s.Ready.Settings.GuildPositions) > 0:
var guilds = make([]cchat.Server, 0, len(s.Ready.Settings.GuildPositions))
for _, id := range s.Ready.Settings.GuildPositions {
g, err := NewGuildFromID(s, id)
if err != nil {
continue
}
guilds = append(guilds, g)
}
container.SetServers(guilds)
// None of the above:
default:
g, err := s.Guilds()
if err != nil {
return err
}
var servers = make([]cchat.Server, len(g))
for i := range g {
servers[i] = NewGuild(s, &g[i])
}
container.SetServers(servers)
}
return nil
}