2020-01-18 07:45:44 +00:00
|
|
|
// Package state provides interfaces for a local or remote state, as well as
|
|
|
|
// abstractions around the REST API and Gateway events.
|
2020-01-17 05:17:46 +00:00
|
|
|
package state
|
|
|
|
|
|
|
|
import (
|
2020-01-20 08:53:23 +00:00
|
|
|
"sync"
|
2020-01-18 21:04:12 +00:00
|
|
|
|
2020-01-17 05:17:46 +00:00
|
|
|
"github.com/diamondburned/arikawa/discord"
|
2020-01-18 21:04:12 +00:00
|
|
|
"github.com/diamondburned/arikawa/gateway"
|
2020-01-17 05:23:56 +00:00
|
|
|
"github.com/diamondburned/arikawa/handler"
|
2020-01-17 05:17:46 +00:00
|
|
|
"github.com/diamondburned/arikawa/session"
|
2020-01-19 06:06:00 +00:00
|
|
|
"github.com/pkg/errors"
|
2020-01-17 05:17:46 +00:00
|
|
|
)
|
|
|
|
|
2020-01-18 19:34:08 +00:00
|
|
|
var (
|
2020-01-19 21:54:16 +00:00
|
|
|
MaxFetchMembers uint = 1000
|
|
|
|
MaxFetchGuilds uint = 100
|
2020-01-18 19:34:08 +00:00
|
|
|
)
|
|
|
|
|
2020-01-17 05:17:46 +00:00
|
|
|
type State struct {
|
|
|
|
*session.Session
|
2020-01-18 21:04:12 +00:00
|
|
|
Store
|
|
|
|
|
2020-01-18 21:40:09 +00:00
|
|
|
// *: State doesn't actually keep track of pinned messages.
|
|
|
|
|
2020-01-18 21:04:12 +00:00
|
|
|
// Ready is not updated by the state.
|
|
|
|
Ready gateway.ReadyEvent
|
|
|
|
|
2020-01-20 09:06:47 +00:00
|
|
|
// StateLog logs all errors that come from the state cache. This includes
|
|
|
|
// not found errors. Defaults to a no-op, as state errors aren't that
|
|
|
|
// important.
|
|
|
|
StateLog func(error)
|
2020-01-17 05:17:46 +00:00
|
|
|
|
|
|
|
// PreHandler is the manual hook that is executed before the State handler
|
|
|
|
// is. This should only be used for low-level operations.
|
|
|
|
// It's recommended to set Synchronous to true if you mutate the events.
|
2020-01-18 07:07:52 +00:00
|
|
|
PreHandler *handler.Handler // default nil
|
2020-01-17 05:17:46 +00:00
|
|
|
|
|
|
|
unhooker func()
|
2020-01-20 08:53:23 +00:00
|
|
|
|
|
|
|
// List of channels with few messages, so it doesn't bother hitting the API
|
|
|
|
// again.
|
|
|
|
fewMessages []discord.Snowflake
|
|
|
|
fewMutex sync.Mutex
|
2020-01-17 05:17:46 +00:00
|
|
|
}
|
|
|
|
|
2020-01-18 07:07:52 +00:00
|
|
|
func NewFromSession(s *session.Session, store Store) (*State, error) {
|
|
|
|
state := &State{
|
2020-01-20 09:06:47 +00:00
|
|
|
Session: s,
|
|
|
|
Store: store,
|
|
|
|
StateLog: func(err error) {},
|
2020-01-18 21:04:12 +00:00
|
|
|
}
|
|
|
|
|
2020-01-20 23:22:34 +00:00
|
|
|
s.ErrorLog = state.ErrorLog
|
2020-01-18 07:07:52 +00:00
|
|
|
|
|
|
|
return state, state.hookSession()
|
|
|
|
}
|
|
|
|
|
|
|
|
func New(token string) (*State, error) {
|
2020-01-25 05:24:33 +00:00
|
|
|
return NewWithStore(token, NewDefaultStore(nil))
|
2020-01-18 07:07:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewWithStore(token string, store Store) (*State, error) {
|
|
|
|
s, err := session.New(token)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-18 21:04:12 +00:00
|
|
|
return NewFromSession(s, store)
|
2020-01-17 05:17:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Unhook removes all state handlers from the session handlers.
|
|
|
|
func (s *State) Unhook() {
|
|
|
|
s.unhooker()
|
|
|
|
}
|
|
|
|
|
2020-01-21 01:06:51 +00:00
|
|
|
//// Helper methods
|
|
|
|
|
|
|
|
func (s *State) AuthorDisplayName(message discord.Message) string {
|
|
|
|
if !message.GuildID.Valid() {
|
|
|
|
return message.Author.Username
|
|
|
|
}
|
|
|
|
|
|
|
|
n, err := s.MemberDisplayName(message.GuildID, message.Author.ID)
|
|
|
|
if err != nil {
|
|
|
|
return message.Author.Username
|
|
|
|
}
|
|
|
|
|
|
|
|
return n
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) MemberDisplayName(
|
|
|
|
guildID, userID discord.Snowflake) (string, error) {
|
|
|
|
|
|
|
|
member, err := s.Member(guildID, userID)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if member.Nick == "" {
|
|
|
|
return member.User.Username, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return member.Nick, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) AuthorColor(message discord.Message) discord.Color {
|
|
|
|
if !message.GuildID.Valid() {
|
|
|
|
return discord.DefaultMemberColor
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.MemberColor(message.GuildID, message.Author.ID)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) MemberColor(guildID, userID discord.Snowflake) discord.Color {
|
|
|
|
member, err := s.Member(guildID, userID)
|
|
|
|
if err != nil {
|
|
|
|
return discord.DefaultMemberColor
|
|
|
|
}
|
|
|
|
|
|
|
|
guild, err := s.Guild(guildID)
|
|
|
|
if err != nil {
|
|
|
|
return discord.DefaultMemberColor
|
|
|
|
}
|
|
|
|
|
|
|
|
return discord.MemberColor(*guild, *member)
|
|
|
|
}
|
|
|
|
|
2020-01-18 07:07:52 +00:00
|
|
|
////
|
|
|
|
|
2020-01-19 06:06:00 +00:00
|
|
|
func (s *State) Permissions(
|
|
|
|
channelID, userID discord.Snowflake) (discord.Permissions, error) {
|
|
|
|
|
|
|
|
ch, err := s.Channel(channelID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, errors.Wrap(err, "Failed to get channel")
|
|
|
|
}
|
|
|
|
|
|
|
|
g, err := s.Guild(ch.GuildID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, errors.Wrap(err, "Failed to get guild")
|
|
|
|
}
|
|
|
|
|
|
|
|
m, err := s.Member(ch.GuildID, userID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, errors.Wrap(err, "Failed to get member")
|
|
|
|
}
|
|
|
|
|
|
|
|
return discord.CalcOverwrites(*g, *ch, *m), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
////
|
|
|
|
|
2020-01-18 07:07:52 +00:00
|
|
|
func (s *State) Self() (*discord.User, error) {
|
|
|
|
u, err := s.Store.Self()
|
|
|
|
if err == nil {
|
|
|
|
return u, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
u, err = s.Session.Me()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-01-17 05:17:46 +00:00
|
|
|
|
2020-01-18 07:10:42 +00:00
|
|
|
return u, s.Store.SelfSet(u)
|
2020-01-17 05:17:46 +00:00
|
|
|
}
|
|
|
|
|
2020-01-18 07:07:52 +00:00
|
|
|
////
|
|
|
|
|
|
|
|
func (s *State) Channel(id discord.Snowflake) (*discord.Channel, error) {
|
|
|
|
c, err := s.Store.Channel(id)
|
|
|
|
if err == nil {
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
c, err = s.Session.Channel(id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, s.Store.ChannelSet(c)
|
|
|
|
}
|
2020-01-17 05:17:46 +00:00
|
|
|
|
2020-01-18 07:07:52 +00:00
|
|
|
func (s *State) Channels(guildID discord.Snowflake) ([]discord.Channel, error) {
|
|
|
|
c, err := s.Store.Channels(guildID)
|
|
|
|
if err == nil {
|
|
|
|
return c, nil
|
|
|
|
}
|
2020-01-17 22:36:53 +00:00
|
|
|
|
2020-01-18 07:07:52 +00:00
|
|
|
c, err = s.Session.Channels(guildID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-01-17 22:36:53 +00:00
|
|
|
|
2020-01-18 07:07:52 +00:00
|
|
|
for _, ch := range c {
|
|
|
|
if err := s.Store.ChannelSet(&ch); err != nil {
|
|
|
|
return nil, err
|
2020-01-17 05:17:46 +00:00
|
|
|
}
|
2020-01-18 07:07:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
////
|
|
|
|
|
2020-01-18 19:34:08 +00:00
|
|
|
func (s *State) Emoji(
|
|
|
|
guildID, emojiID discord.Snowflake) (*discord.Emoji, error) {
|
|
|
|
|
|
|
|
e, err := s.Store.Emoji(guildID, emojiID)
|
|
|
|
if err == nil {
|
|
|
|
return e, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
es, err := s.Session.Emojis(guildID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.Store.EmojiSet(guildID, es); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, e := range es {
|
|
|
|
if e.ID == emojiID {
|
|
|
|
return &e, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, ErrStoreNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) Emojis(guildID discord.Snowflake) ([]discord.Emoji, error) {
|
|
|
|
e, err := s.Store.Emojis(guildID)
|
|
|
|
if err == nil {
|
|
|
|
return e, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
es, err := s.Session.Emojis(guildID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return es, s.Store.EmojiSet(guildID, es)
|
|
|
|
}
|
|
|
|
|
|
|
|
////
|
|
|
|
|
|
|
|
func (s *State) Guild(id discord.Snowflake) (*discord.Guild, error) {
|
|
|
|
c, err := s.Store.Guild(id)
|
|
|
|
if err == nil {
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
c, err = s.Session.Guild(id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, s.Store.GuildSet(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Guilds will only fill a maximum of 100 guilds from the API.
|
|
|
|
func (s *State) Guilds() ([]discord.Guild, error) {
|
|
|
|
c, err := s.Store.Guilds()
|
|
|
|
if err == nil {
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
2020-01-19 21:54:16 +00:00
|
|
|
c, err = s.Session.Guilds(MaxFetchGuilds)
|
2020-01-18 19:34:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ch := range c {
|
|
|
|
if err := s.Store.GuildSet(&ch); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
////
|
|
|
|
|
|
|
|
func (s *State) Member(
|
|
|
|
guildID, userID discord.Snowflake) (*discord.Member, error) {
|
|
|
|
|
|
|
|
m, err := s.Store.Member(guildID, userID)
|
|
|
|
if err == nil {
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
m, err = s.Session.Member(guildID, userID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-21 04:25:47 +00:00
|
|
|
return m, s.Store.MemberSet(guildID, m)
|
2020-01-18 19:34:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) Members(guildID discord.Snowflake) ([]discord.Member, error) {
|
|
|
|
ms, err := s.Store.Members(guildID)
|
|
|
|
if err == nil {
|
|
|
|
return ms, nil
|
|
|
|
}
|
|
|
|
|
2020-01-19 21:54:16 +00:00
|
|
|
ms, err = s.Session.Members(guildID, MaxFetchMembers)
|
2020-01-18 19:34:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, m := range ms {
|
|
|
|
if err := s.Store.MemberSet(guildID, &m); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-19 21:54:16 +00:00
|
|
|
return ms, s.Gateway.RequestGuildMembers(gateway.RequestGuildMembersData{
|
|
|
|
GuildID: []discord.Snowflake{guildID},
|
|
|
|
Presences: true,
|
|
|
|
})
|
2020-01-18 19:34:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
////
|
|
|
|
|
|
|
|
func (s *State) Message(
|
|
|
|
channelID, messageID discord.Snowflake) (*discord.Message, error) {
|
|
|
|
|
|
|
|
m, err := s.Store.Message(channelID, messageID)
|
|
|
|
if err == nil {
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
m, err = s.Session.Message(channelID, messageID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-25 08:05:14 +00:00
|
|
|
// Fill the GuildID, because Discord doesn't do it for us.
|
|
|
|
c, err := s.Channel(channelID)
|
|
|
|
if err == nil {
|
|
|
|
// If it's 0, it's 0 anyway. We don't need a check here.
|
|
|
|
m.GuildID = c.GuildID
|
|
|
|
}
|
|
|
|
|
2020-01-18 19:34:08 +00:00
|
|
|
return m, s.Store.MessageSet(m)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Messages fetches maximum 100 messages from the API, if it has to. There is no
|
|
|
|
// limit if it's from the State storage.
|
|
|
|
func (s *State) Messages(channelID discord.Snowflake) ([]discord.Message, error) {
|
2020-01-20 08:53:23 +00:00
|
|
|
// TODO: Think of a design that doesn't rely on MaxMessages().
|
|
|
|
var maxMsgs = s.MaxMessages()
|
|
|
|
|
2020-01-18 19:34:08 +00:00
|
|
|
ms, err := s.Store.Messages(channelID)
|
|
|
|
if err == nil {
|
2020-01-20 08:53:23 +00:00
|
|
|
// If the state already has as many messages as it can, skip the API.
|
|
|
|
if maxMsgs <= len(ms) {
|
|
|
|
return ms, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Is the channel tiny?
|
|
|
|
s.fewMutex.Lock()
|
|
|
|
for _, ch := range s.fewMessages {
|
|
|
|
if ch == channelID {
|
|
|
|
// Yes, skip the state.
|
|
|
|
s.fewMutex.Unlock()
|
|
|
|
return ms, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No, fetch from the state.
|
|
|
|
s.fewMutex.Unlock()
|
2020-01-18 19:34:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ms, err = s.Session.Messages(channelID, 100)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-01-20 23:22:34 +00:00
|
|
|
// New messages fetched weirdly does not have GuildID filled. We'll try and
|
|
|
|
// get it for consistency with incoming message creates.
|
|
|
|
var guildID discord.Snowflake
|
|
|
|
|
|
|
|
// A bit too convoluted, but whatever.
|
|
|
|
c, err := s.Channel(channelID)
|
|
|
|
if err == nil {
|
|
|
|
// If it's 0, it's 0 anyway. We don't need a check here.
|
|
|
|
guildID = c.GuildID
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range ms {
|
|
|
|
// Set the guild ID, fine if it's 0 (it's already 0 anyway).
|
|
|
|
ms[i].GuildID = guildID
|
|
|
|
|
|
|
|
if err := s.Store.MessageSet(&ms[i]); err != nil {
|
2020-01-18 19:34:08 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-20 08:53:23 +00:00
|
|
|
if len(ms) < maxMsgs {
|
|
|
|
// Tiny channel, store this.
|
|
|
|
s.fewMutex.Lock()
|
|
|
|
s.fewMessages = append(s.fewMessages, channelID)
|
|
|
|
s.fewMutex.Unlock()
|
|
|
|
|
|
|
|
return ms, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Since the latest messages are at the end and we already know the maxMsgs,
|
|
|
|
// we could slice this right away.
|
|
|
|
return ms[:maxMsgs], nil
|
2020-01-18 19:34:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
////
|
|
|
|
|
|
|
|
func (s *State) Presence(
|
|
|
|
guildID, userID discord.Snowflake) (*discord.Presence, error) {
|
|
|
|
|
|
|
|
return s.Store.Presence(guildID, userID)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) Presences(
|
|
|
|
guildID discord.Snowflake) ([]discord.Presence, error) {
|
|
|
|
|
|
|
|
return s.Store.Presences(guildID)
|
|
|
|
}
|
|
|
|
|
|
|
|
////
|
|
|
|
|
|
|
|
func (s *State) Role(
|
|
|
|
guildID, roleID discord.Snowflake) (*discord.Role, error) {
|
|
|
|
|
|
|
|
r, err := s.Store.Role(guildID, roleID)
|
|
|
|
if err == nil {
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
rs, err := s.Session.Roles(guildID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var role *discord.Role
|
|
|
|
|
|
|
|
for _, r := range rs {
|
|
|
|
if r.ID == roleID {
|
|
|
|
role = &r
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.RoleSet(guildID, &r); err != nil {
|
|
|
|
return role, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return role, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *State) Roles(guildID discord.Snowflake) ([]discord.Role, error) {
|
|
|
|
rs, err := s.Store.Roles(guildID)
|
|
|
|
if err == nil {
|
|
|
|
return rs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
rs, err = s.Session.Roles(guildID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, r := range rs {
|
|
|
|
if err := s.RoleSet(guildID, &r); err != nil {
|
|
|
|
return rs, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return rs, nil
|
|
|
|
}
|