Merge branch 'develop' into 1
This commit is contained in:
parent
27db9ad6df
commit
b813c5d0ca
14 changed files with 357 additions and 102 deletions
|
@ -10,5 +10,5 @@ install:
|
||||||
script:
|
script:
|
||||||
- diff <(gofmt -d .) <(echo -n)
|
- diff <(gofmt -d .) <(echo -n)
|
||||||
- go vet -x ./...
|
- go vet -x ./...
|
||||||
- golint -set_exit_status ./...
|
- golint ./...
|
||||||
- go test -v -race ./...
|
- go test -v -race ./...
|
||||||
|
|
|
@ -90,7 +90,6 @@ that information in a nice format.
|
||||||
|
|
||||||
- [](https://godoc.org/github.com/bwmarrin/discordgo)
|
- [](https://godoc.org/github.com/bwmarrin/discordgo)
|
||||||
- [](https://gowalker.org/github.com/bwmarrin/discordgo)
|
- [](https://gowalker.org/github.com/bwmarrin/discordgo)
|
||||||
- [Unofficial Discord API Documentation](https://discordapi.readthedocs.org/en/latest/)
|
|
||||||
- Hand crafted documentation coming eventually.
|
- Hand crafted documentation coming eventually.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/)
|
// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
|
||||||
const VERSION = "0.16.0-dev"
|
const VERSION = "0.17.0-dev"
|
||||||
|
|
||||||
// ErrMFA will be risen by New when the user has 2FA.
|
// ErrMFA will be risen by New when the user has 2FA.
|
||||||
var ErrMFA = errors.New("account has 2FA enabled")
|
var ErrMFA = errors.New("account has 2FA enabled")
|
||||||
|
@ -59,6 +59,7 @@ func New(args ...interface{}) (s *Session, err error) {
|
||||||
MaxRestRetries: 3,
|
MaxRestRetries: 3,
|
||||||
Client: &http.Client{Timeout: (20 * time.Second)},
|
Client: &http.Client{Timeout: (20 * time.Second)},
|
||||||
sequence: new(int64),
|
sequence: new(int64),
|
||||||
|
LastHeartbeatAck: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no arguments are passed return the empty Session interface.
|
// If no arguments are passed return the empty Session interface.
|
||||||
|
|
|
@ -11,9 +11,11 @@ import (
|
||||||
//////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////////////////////////////////// VARS NEEDED FOR TESTING
|
////////////////////////////////////////////////////// VARS NEEDED FOR TESTING
|
||||||
var (
|
var (
|
||||||
dg *Session // Stores global discordgo session
|
dg *Session // Stores a global discordgo user session
|
||||||
|
dgBot *Session // Stores a global discordgo bot session
|
||||||
|
|
||||||
envToken = os.Getenv("DG_TOKEN") // Token to use when authenticating
|
envToken = os.Getenv("DG_TOKEN") // Token to use when authenticating the user account
|
||||||
|
envBotToken = os.Getenv("DGB_TOKEN") // Token to use when authenticating the bot account
|
||||||
envEmail = os.Getenv("DG_EMAIL") // Email to use when authenticating
|
envEmail = os.Getenv("DG_EMAIL") // Email to use when authenticating
|
||||||
envPassword = os.Getenv("DG_PASSWORD") // Password to use when authenticating
|
envPassword = os.Getenv("DG_PASSWORD") // Password to use when authenticating
|
||||||
envGuild = os.Getenv("DG_GUILD") // Guild ID to use for tests
|
envGuild = os.Getenv("DG_GUILD") // Guild ID to use for tests
|
||||||
|
@ -23,6 +25,12 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
if envBotToken != "" {
|
||||||
|
if d, err := New(envBotToken); err == nil {
|
||||||
|
dgBot = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if envEmail == "" || envPassword == "" || envToken == "" {
|
if envEmail == "" || envPassword == "" || envToken == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
36
endpoints.go
36
endpoints.go
|
@ -18,13 +18,14 @@ var (
|
||||||
EndpointSmActive = EndpointSm + "active.json"
|
EndpointSmActive = EndpointSm + "active.json"
|
||||||
EndpointSmUpcoming = EndpointSm + "upcoming.json"
|
EndpointSmUpcoming = EndpointSm + "upcoming.json"
|
||||||
|
|
||||||
EndpointDiscord = "https://discordapp.com/"
|
EndpointDiscord = "https://discordapp.com/"
|
||||||
EndpointAPI = EndpointDiscord + "api/"
|
EndpointAPI = EndpointDiscord + "api/"
|
||||||
EndpointGuilds = EndpointAPI + "guilds/"
|
EndpointGuilds = EndpointAPI + "guilds/"
|
||||||
EndpointChannels = EndpointAPI + "channels/"
|
EndpointChannels = EndpointAPI + "channels/"
|
||||||
EndpointUsers = EndpointAPI + "users/"
|
EndpointUsers = EndpointAPI + "users/"
|
||||||
EndpointGateway = EndpointAPI + "gateway"
|
EndpointGateway = EndpointAPI + "gateway"
|
||||||
EndpointWebhooks = EndpointAPI + "webhooks/"
|
EndpointGatewayBot = EndpointGateway + "/bot"
|
||||||
|
EndpointWebhooks = EndpointAPI + "webhooks/"
|
||||||
|
|
||||||
EndpointCDN = "https://cdn.discordapp.com/"
|
EndpointCDN = "https://cdn.discordapp.com/"
|
||||||
EndpointCDNAttachments = EndpointCDN + "attachments/"
|
EndpointCDNAttachments = EndpointCDN + "attachments/"
|
||||||
|
@ -54,16 +55,17 @@ var (
|
||||||
EndpointReport = EndpointAPI + "report"
|
EndpointReport = EndpointAPI + "report"
|
||||||
EndpointIntegrations = EndpointAPI + "integrations"
|
EndpointIntegrations = EndpointAPI + "integrations"
|
||||||
|
|
||||||
EndpointUser = func(uID string) string { return EndpointUsers + uID }
|
EndpointUser = func(uID string) string { return EndpointUsers + uID }
|
||||||
EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" }
|
EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" }
|
||||||
EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" }
|
EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" }
|
||||||
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
|
EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" }
|
||||||
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
|
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
|
||||||
EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" }
|
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
|
||||||
EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" }
|
EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" }
|
||||||
EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" }
|
EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" }
|
||||||
EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" }
|
EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" }
|
||||||
EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID }
|
EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" }
|
||||||
|
EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID }
|
||||||
|
|
||||||
EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
|
EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
|
||||||
EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" }
|
EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" }
|
||||||
|
|
77
message.go
77
message.go
|
@ -12,6 +12,7 @@ package discordgo
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A Message stores all data related to a specific Discord message.
|
// A Message stores all data related to a specific Discord message.
|
||||||
|
@ -33,8 +34,9 @@ type Message struct {
|
||||||
|
|
||||||
// File stores info about files you e.g. send in messages.
|
// File stores info about files you e.g. send in messages.
|
||||||
type File struct {
|
type File struct {
|
||||||
Name string
|
Name string
|
||||||
Reader io.Reader
|
ContentType string
|
||||||
|
Reader io.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageSend stores all parameters you can send with ChannelMessageSendComplex.
|
// MessageSend stores all parameters you can send with ChannelMessageSendComplex.
|
||||||
|
@ -42,7 +44,10 @@ type MessageSend struct {
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
Embed *MessageEmbed `json:"embed,omitempty"`
|
Embed *MessageEmbed `json:"embed,omitempty"`
|
||||||
Tts bool `json:"tts"`
|
Tts bool `json:"tts"`
|
||||||
File *File `json:"file"`
|
Files []*File `json:"-"`
|
||||||
|
|
||||||
|
// TODO: Remove this when compatibility is not required.
|
||||||
|
File *File `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageEdit is used to chain parameters via ChannelMessageEditComplex, which
|
// MessageEdit is used to chain parameters via ChannelMessageEditComplex, which
|
||||||
|
@ -167,13 +172,65 @@ type MessageReactions struct {
|
||||||
|
|
||||||
// ContentWithMentionsReplaced will replace all @<id> mentions with the
|
// ContentWithMentionsReplaced will replace all @<id> mentions with the
|
||||||
// username of the mention.
|
// username of the mention.
|
||||||
func (m *Message) ContentWithMentionsReplaced() string {
|
func (m *Message) ContentWithMentionsReplaced() (content string) {
|
||||||
if m.Mentions == nil {
|
content = m.Content
|
||||||
return m.Content
|
|
||||||
}
|
|
||||||
content := m.Content
|
|
||||||
for _, user := range m.Mentions {
|
for _, user := range m.Mentions {
|
||||||
content = regexp.MustCompile("<@!?("+regexp.QuoteMeta(user.ID)+")>").ReplaceAllString(content, "@"+user.Username)
|
content = strings.NewReplacer(
|
||||||
|
"<@"+user.ID+">", "@"+user.Username,
|
||||||
|
"<@!"+user.ID+">", "@"+user.Username,
|
||||||
|
).Replace(content)
|
||||||
}
|
}
|
||||||
return content
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var patternChannels = regexp.MustCompile("<#[^>]*>")
|
||||||
|
|
||||||
|
// ContentWithMoreMentionsReplaced will replace all @<id> mentions with the
|
||||||
|
// username of the mention, but also role IDs and more.
|
||||||
|
func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, err error) {
|
||||||
|
content = m.Content
|
||||||
|
|
||||||
|
if !s.StateEnabled {
|
||||||
|
content = m.ContentWithMentionsReplaced()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, err := s.State.Channel(m.ChannelID)
|
||||||
|
if err != nil {
|
||||||
|
content = m.ContentWithMentionsReplaced()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range m.Mentions {
|
||||||
|
nick := user.Username
|
||||||
|
|
||||||
|
member, err := s.State.Member(channel.GuildID, user.ID)
|
||||||
|
if err == nil && member.Nick != "" {
|
||||||
|
nick = member.Nick
|
||||||
|
}
|
||||||
|
|
||||||
|
content = strings.NewReplacer(
|
||||||
|
"<@"+user.ID+">", "@"+user.Username,
|
||||||
|
"<@!"+user.ID+">", "@"+nick,
|
||||||
|
).Replace(content)
|
||||||
|
}
|
||||||
|
for _, roleID := range m.MentionRoles {
|
||||||
|
role, err := s.State.Role(channel.GuildID, roleID)
|
||||||
|
if err != nil || !role.Mentionable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string {
|
||||||
|
channel, err := s.State.Channel(mention[2 : len(mention)-1])
|
||||||
|
if err != nil || channel.Type == "voice" {
|
||||||
|
return mention
|
||||||
|
}
|
||||||
|
|
||||||
|
return "#" + channel.Name
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
41
message_test.go
Normal file
41
message_test.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package discordgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContentWithMoreMentionsReplaced(t *testing.T) {
|
||||||
|
s := &Session{StateEnabled: true, State: NewState()}
|
||||||
|
|
||||||
|
user := &User{
|
||||||
|
ID: "user",
|
||||||
|
Username: "User Name",
|
||||||
|
}
|
||||||
|
|
||||||
|
s.StateEnabled = true
|
||||||
|
s.State.GuildAdd(&Guild{ID: "guild"})
|
||||||
|
s.State.RoleAdd("guild", &Role{
|
||||||
|
ID: "role",
|
||||||
|
Name: "Role Name",
|
||||||
|
Mentionable: true,
|
||||||
|
})
|
||||||
|
s.State.MemberAdd(&Member{
|
||||||
|
User: user,
|
||||||
|
Nick: "User Nick",
|
||||||
|
GuildID: "guild",
|
||||||
|
})
|
||||||
|
s.State.ChannelAdd(&Channel{
|
||||||
|
Name: "Channel Name",
|
||||||
|
GuildID: "guild",
|
||||||
|
ID: "channel",
|
||||||
|
})
|
||||||
|
m := &Message{
|
||||||
|
Content: "<&role> <@!user> <@user> <#channel>",
|
||||||
|
ChannelID: "channel",
|
||||||
|
MentionRoles: []string{"role"},
|
||||||
|
Mentions: []*User{user},
|
||||||
|
}
|
||||||
|
if result, _ := m.ContentWithMoreMentionsReplaced(s); result != "@Role Name @User Nick @User Name #Channel Name" {
|
||||||
|
t.Error(result)
|
||||||
|
}
|
||||||
|
}
|
142
restapi.go
142
restapi.go
|
@ -23,14 +23,22 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrJSONUnmarshal is returned for JSON Unmarshall errors.
|
// All error constants
|
||||||
var ErrJSONUnmarshal = errors.New("json unmarshal")
|
var (
|
||||||
|
ErrJSONUnmarshal = errors.New("json unmarshal")
|
||||||
|
ErrStatusOffline = errors.New("You can't set your Status to offline")
|
||||||
|
ErrVerificationLevelBounds = errors.New("VerificationLevel out of bounds, should be between 0 and 3")
|
||||||
|
ErrPruneDaysBounds = errors.New("the number of days should be more than or equal to 1")
|
||||||
|
ErrGuildNoIcon = errors.New("guild does not have an icon set")
|
||||||
|
ErrGuildNoSplash = errors.New("guild does not have a splash set")
|
||||||
|
)
|
||||||
|
|
||||||
// Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr
|
// Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr
|
||||||
func (s *Session) Request(method, urlStr string, data interface{}) (response []byte, err error) {
|
func (s *Session) Request(method, urlStr string, data interface{}) (response []byte, err error) {
|
||||||
|
@ -302,8 +310,8 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri
|
||||||
// If left blank, avatar will be set to null/blank
|
// If left blank, avatar will be set to null/blank
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email,omitempty"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Avatar string `json:"avatar,omitempty"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
NewPassword string `json:"new_password,omitempty"`
|
NewPassword string `json:"new_password,omitempty"`
|
||||||
|
@ -334,7 +342,7 @@ func (s *Session) UserSettings() (st *Settings, err error) {
|
||||||
// status : The new status (Actual valid status are 'online','idle','dnd','invisible')
|
// status : The new status (Actual valid status are 'online','idle','dnd','invisible')
|
||||||
func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) {
|
func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) {
|
||||||
if status == StatusOffline {
|
if status == StatusOffline {
|
||||||
err = errors.New("You can't set your Status to offline")
|
err = ErrStatusOffline
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -595,7 +603,7 @@ func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error
|
||||||
if g.VerificationLevel != nil {
|
if g.VerificationLevel != nil {
|
||||||
val := *g.VerificationLevel
|
val := *g.VerificationLevel
|
||||||
if val < 0 || val > 3 {
|
if val < 0 || val > 3 {
|
||||||
err = errors.New("VerificationLevel out of bounds, should be between 0 and 3")
|
err = ErrVerificationLevelBounds
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -756,7 +764,21 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) {
|
||||||
// userID : The ID of a User
|
// userID : The ID of a User
|
||||||
func (s *Session) GuildMemberDelete(guildID, userID string) (err error) {
|
func (s *Session) GuildMemberDelete(guildID, userID string) (err error) {
|
||||||
|
|
||||||
_, err = s.RequestWithBucketID("DELETE", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, ""))
|
return s.GuildMemberDeleteWithReason(guildID, userID, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GuildMemberDelete removes the given user from the given guild.
|
||||||
|
// guildID : The ID of a Guild.
|
||||||
|
// userID : The ID of a User
|
||||||
|
// reason : The reason for the kick
|
||||||
|
func (s *Session) GuildMemberDeleteWithReason(guildID, userID, reason string) (err error) {
|
||||||
|
|
||||||
|
uri := EndpointGuildMember(guildID, userID)
|
||||||
|
if reason != "" {
|
||||||
|
uri += "?reason=" + url.QueryEscape(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointGuildMember(guildID, ""))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -988,7 +1010,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
if days <= 0 {
|
if days <= 0 {
|
||||||
err = errors.New("the number of days should be more than or equal to 1")
|
err = ErrPruneDaysBounds
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1018,7 +1040,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
if days <= 0 {
|
if days <= 0 {
|
||||||
err = errors.New("the number of days should be more than or equal to 1")
|
err = ErrPruneDaysBounds
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1120,7 +1142,7 @@ func (s *Session) GuildIcon(guildID string) (img image.Image, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.Icon == "" {
|
if g.Icon == "" {
|
||||||
err = errors.New("guild does not have an icon set")
|
err = ErrGuildNoIcon
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1142,7 +1164,7 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.Splash == "" {
|
if g.Splash == "" {
|
||||||
err = errors.New("guild does not have a splash set")
|
err = ErrGuildNoSplash
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1309,6 +1331,8 @@ func (s *Session) ChannelMessageSend(channelID string, content string) (*Message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||||
|
|
||||||
// ChannelMessageSendComplex sends a message to the given channel.
|
// ChannelMessageSendComplex sends a message to the given channel.
|
||||||
// channelID : The ID of a Channel.
|
// channelID : The ID of a Channel.
|
||||||
// data : The message struct to send.
|
// data : The message struct to send.
|
||||||
|
@ -1319,48 +1343,62 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend)
|
||||||
|
|
||||||
endpoint := EndpointChannelMessages(channelID)
|
endpoint := EndpointChannelMessages(channelID)
|
||||||
|
|
||||||
var response []byte
|
// TODO: Remove this when compatibility is not required.
|
||||||
|
files := data.Files
|
||||||
if data.File != nil {
|
if data.File != nil {
|
||||||
|
if files == nil {
|
||||||
|
files = []*File{data.File}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("cannot specify both File and Files")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var response []byte
|
||||||
|
if len(files) > 0 {
|
||||||
body := &bytes.Buffer{}
|
body := &bytes.Buffer{}
|
||||||
bodywriter := multipart.NewWriter(body)
|
bodywriter := multipart.NewWriter(body)
|
||||||
|
|
||||||
// What's a better way of doing this? Reflect? Generator? I'm open to suggestions
|
var payload []byte
|
||||||
|
payload, err = json.Marshal(data)
|
||||||
if data.Content != "" {
|
|
||||||
if err = bodywriter.WriteField("content", data.Content); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Embed != nil {
|
|
||||||
var embed []byte
|
|
||||||
embed, err = json.Marshal(data.Embed)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = bodywriter.WriteField("embed", string(embed))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Tts {
|
|
||||||
if err = bodywriter.WriteField("tts", "true"); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var writer io.Writer
|
|
||||||
writer, err = bodywriter.CreateFormFile("file", data.File.Name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(writer, data.File.Reader)
|
var p io.Writer
|
||||||
|
|
||||||
|
h := make(textproto.MIMEHeader)
|
||||||
|
h.Set("Content-Disposition", `form-data; name="payload_json"`)
|
||||||
|
h.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
p, err = bodywriter.CreatePart(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err = p.Write(payload); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, file := range files {
|
||||||
|
h := make(textproto.MIMEHeader)
|
||||||
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name)))
|
||||||
|
contentType := file.ContentType
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
h.Set("Content-Type", contentType)
|
||||||
|
|
||||||
|
p, err = bodywriter.CreatePart(h)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = io.Copy(p, file.Reader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = bodywriter.Close()
|
err = bodywriter.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -1678,6 +1716,28 @@ func (s *Session) Gateway() (gateway string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GatewayBot returns the websocket Gateway address and the recommended number of shards
|
||||||
|
func (s *Session) GatewayBot() (st *GatewayBotResponse, err error) {
|
||||||
|
|
||||||
|
response, err := s.RequestWithBucketID("GET", EndpointGatewayBot, nil, EndpointGatewayBot)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = unmarshal(response, &st)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the gateway always has a trailing slash.
|
||||||
|
// MacOS will fail to connect if we add query params without a trailing slash on the base domain.
|
||||||
|
if !strings.HasSuffix(st.URL, "/") {
|
||||||
|
st.URL += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Functions specific to Webhooks
|
// Functions specific to Webhooks
|
||||||
|
|
||||||
// WebhookCreate returns a new Webhook.
|
// WebhookCreate returns a new Webhook.
|
||||||
|
|
|
@ -166,6 +166,17 @@ func TestGateway(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGatewayBot(t *testing.T) {
|
||||||
|
|
||||||
|
if dgBot == nil {
|
||||||
|
t.Skip("Skipping, dgBot not set.")
|
||||||
|
}
|
||||||
|
_, err := dgBot.GatewayBot()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GatewayBot() returned error: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVoiceICE(t *testing.T) {
|
func TestVoiceICE(t *testing.T) {
|
||||||
|
|
||||||
if dg == nil {
|
if dg == nil {
|
||||||
|
|
28
state.go
28
state.go
|
@ -21,6 +21,10 @@ import (
|
||||||
// ErrNilState is returned when the state is nil.
|
// ErrNilState is returned when the state is nil.
|
||||||
var ErrNilState = errors.New("state not instantiated, please use discordgo.New() or assign Session.State")
|
var ErrNilState = errors.New("state not instantiated, please use discordgo.New() or assign Session.State")
|
||||||
|
|
||||||
|
// ErrStateNotFound is returned when the state cache
|
||||||
|
// requested is not found
|
||||||
|
var ErrStateNotFound = errors.New("state cache not found")
|
||||||
|
|
||||||
// A State contains the current known state.
|
// A State contains the current known state.
|
||||||
// As discord sends this in a READY blob, it seems reasonable to simply
|
// As discord sends this in a READY blob, it seems reasonable to simply
|
||||||
// use that struct as the data store.
|
// use that struct as the data store.
|
||||||
|
@ -146,7 +150,7 @@ func (s *State) Guild(guildID string) (*Guild, error) {
|
||||||
return g, nil
|
return g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("guild not found")
|
return nil, ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// PresenceAdd adds a presence to the current world state, or
|
// PresenceAdd adds a presence to the current world state, or
|
||||||
|
@ -227,7 +231,7 @@ func (s *State) PresenceRemove(guildID string, presence *Presence) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("presence not found")
|
return ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Presence gets a presence by ID from a guild.
|
// Presence gets a presence by ID from a guild.
|
||||||
|
@ -247,7 +251,7 @@ func (s *State) Presence(guildID, userID string) (*Presence, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("presence not found")
|
return nil, ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Consider moving Guild state update methods onto *Guild.
|
// TODO: Consider moving Guild state update methods onto *Guild.
|
||||||
|
@ -299,7 +303,7 @@ func (s *State) MemberRemove(member *Member) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("member not found")
|
return ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Member gets a member by ID from a guild.
|
// Member gets a member by ID from a guild.
|
||||||
|
@ -322,7 +326,7 @@ func (s *State) Member(guildID, userID string) (*Member, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("member not found")
|
return nil, ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoleAdd adds a role to the current world state, or
|
// RoleAdd adds a role to the current world state, or
|
||||||
|
@ -372,7 +376,7 @@ func (s *State) RoleRemove(guildID, roleID string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("role not found")
|
return ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Role gets a role by ID from a guild.
|
// Role gets a role by ID from a guild.
|
||||||
|
@ -395,7 +399,7 @@ func (s *State) Role(guildID, roleID string) (*Role, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("role not found")
|
return nil, ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelAdd adds a channel to the current world state, or
|
// ChannelAdd adds a channel to the current world state, or
|
||||||
|
@ -428,7 +432,7 @@ func (s *State) ChannelAdd(channel *Channel) error {
|
||||||
} else {
|
} else {
|
||||||
guild, ok := s.guildMap[channel.GuildID]
|
guild, ok := s.guildMap[channel.GuildID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("guild for channel not found")
|
return ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
guild.Channels = append(guild.Channels, channel)
|
guild.Channels = append(guild.Channels, channel)
|
||||||
|
@ -507,7 +511,7 @@ func (s *State) Channel(channelID string) (*Channel, error) {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("channel not found")
|
return nil, ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emoji returns an emoji for a guild and emoji id.
|
// Emoji returns an emoji for a guild and emoji id.
|
||||||
|
@ -530,7 +534,7 @@ func (s *State) Emoji(guildID, emojiID string) (*Emoji, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("emoji not found")
|
return nil, ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmojiAdd adds an emoji to the current world state.
|
// EmojiAdd adds an emoji to the current world state.
|
||||||
|
@ -647,7 +651,7 @@ func (s *State) messageRemoveByID(channelID, messageID string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("message not found")
|
return ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error {
|
func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error {
|
||||||
|
@ -701,7 +705,7 @@ func (s *State) Message(channelID, messageID string) (*Message, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("message not found")
|
return nil, ErrStateNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnReady takes a Ready event and updates all internal state.
|
// OnReady takes a Ready event and updates all internal state.
|
||||||
|
|
19
structs.go
19
structs.go
|
@ -78,6 +78,9 @@ type Session struct {
|
||||||
// The http client used for REST requests
|
// The http client used for REST requests
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
|
|
||||||
|
// Stores the last HeartbeatAck that was recieved (in UTC)
|
||||||
|
LastHeartbeatAck time.Time
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
handlersMu sync.RWMutex
|
handlersMu sync.RWMutex
|
||||||
handlers map[string][]*eventHandlerInstance
|
handlers map[string][]*eventHandlerInstance
|
||||||
|
@ -304,7 +307,7 @@ type Game struct {
|
||||||
// UnmarshalJSON unmarshals json to Game struct
|
// UnmarshalJSON unmarshals json to Game struct
|
||||||
func (g *Game) UnmarshalJSON(bytes []byte) error {
|
func (g *Game) UnmarshalJSON(bytes []byte) error {
|
||||||
temp := &struct {
|
temp := &struct {
|
||||||
Name string `json:"name"`
|
Name json.Number `json:"name"`
|
||||||
Type json.RawMessage `json:"type"`
|
Type json.RawMessage `json:"type"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}{}
|
}{}
|
||||||
|
@ -312,8 +315,8 @@ func (g *Game) UnmarshalJSON(bytes []byte) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
g.Name = temp.Name
|
|
||||||
g.URL = temp.URL
|
g.URL = temp.URL
|
||||||
|
g.Name = temp.Name.String()
|
||||||
|
|
||||||
if temp.Type != nil {
|
if temp.Type != nil {
|
||||||
err = json.Unmarshal(temp.Type, &g.Type)
|
err = json.Unmarshal(temp.Type, &g.Type)
|
||||||
|
@ -509,6 +512,12 @@ type MessageReaction struct {
|
||||||
ChannelID string `json:"channel_id"`
|
ChannelID string `json:"channel_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GatewayBotResponse stores the data for the gateway/bot response
|
||||||
|
type GatewayBotResponse struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Shards int `json:"shards"`
|
||||||
|
}
|
||||||
|
|
||||||
// Constants for the different bit offsets of text channel permissions
|
// Constants for the different bit offsets of text channel permissions
|
||||||
const (
|
const (
|
||||||
PermissionReadMessages = 1 << (iota + 10)
|
PermissionReadMessages = 1 << (iota + 10)
|
||||||
|
@ -549,6 +558,8 @@ const (
|
||||||
PermissionAdministrator
|
PermissionAdministrator
|
||||||
PermissionManageChannels
|
PermissionManageChannels
|
||||||
PermissionManageServer
|
PermissionManageServer
|
||||||
|
PermissionAddReactions
|
||||||
|
PermissionViewAuditLogs
|
||||||
|
|
||||||
PermissionAllText = PermissionReadMessages |
|
PermissionAllText = PermissionReadMessages |
|
||||||
PermissionSendMessages |
|
PermissionSendMessages |
|
||||||
|
@ -568,7 +579,9 @@ const (
|
||||||
PermissionAllVoice |
|
PermissionAllVoice |
|
||||||
PermissionCreateInstantInvite |
|
PermissionCreateInstantInvite |
|
||||||
PermissionManageRoles |
|
PermissionManageRoles |
|
||||||
PermissionManageChannels
|
PermissionManageChannels |
|
||||||
|
PermissionAddReactions |
|
||||||
|
PermissionViewAuditLogs
|
||||||
PermissionAll = PermissionAllChannel |
|
PermissionAll = PermissionAllChannel |
|
||||||
PermissionKickMembers |
|
PermissionKickMembers |
|
||||||
PermissionBanMembers |
|
PermissionBanMembers |
|
||||||
|
|
15
user.go
15
user.go
|
@ -1,5 +1,7 @@
|
||||||
package discordgo
|
package discordgo
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
// A User stores all data for an individual Discord user.
|
// A User stores all data for an individual Discord user.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
@ -22,3 +24,16 @@ func (u *User) String() string {
|
||||||
func (u *User) Mention() string {
|
func (u *User) Mention() string {
|
||||||
return "<@" + u.ID + ">"
|
return "<@" + u.ID + ">"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AvatarURL returns a URL to the user's avatar.
|
||||||
|
// size: The size of the user's avatar as a power of two
|
||||||
|
func (u *User) AvatarURL(size string) string {
|
||||||
|
var URL string
|
||||||
|
if strings.HasPrefix(u.Avatar, "a_") {
|
||||||
|
URL = EndpointUserAvatarAnimated(u.ID, u.Avatar)
|
||||||
|
} else {
|
||||||
|
URL = EndpointUserAvatar(u.ID, u.Avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
return URL + "?size=" + size
|
||||||
|
}
|
||||||
|
|
6
voice.go
6
voice.go
|
@ -814,7 +814,11 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct
|
||||||
p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey)
|
p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey)
|
||||||
|
|
||||||
if c != nil {
|
if c != nil {
|
||||||
c <- &p
|
select {
|
||||||
|
case c <- &p:
|
||||||
|
case <-close:
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
64
wsapi.go
64
wsapi.go
|
@ -25,6 +25,18 @@ import (
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrWSAlreadyOpen is thrown when you attempt to open
|
||||||
|
// a websocket that already is open.
|
||||||
|
var ErrWSAlreadyOpen = errors.New("web socket already opened")
|
||||||
|
|
||||||
|
// ErrWSNotFound is thrown when you attempt to use a websocket
|
||||||
|
// that doesn't exist
|
||||||
|
var ErrWSNotFound = errors.New("no websocket connection exists")
|
||||||
|
|
||||||
|
// ErrWSShardBounds is thrown when you try to use a shard ID that is
|
||||||
|
// less than the total shard count
|
||||||
|
var ErrWSShardBounds = errors.New("ShardID must be less than ShardCount")
|
||||||
|
|
||||||
type resumePacket struct {
|
type resumePacket struct {
|
||||||
Op int `json:"op"`
|
Op int `json:"op"`
|
||||||
Data struct {
|
Data struct {
|
||||||
|
@ -58,7 +70,7 @@ func (s *Session) Open() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.wsConn != nil {
|
if s.wsConn != nil {
|
||||||
err = errors.New("web socket already opened")
|
err = ErrWSAlreadyOpen
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +131,7 @@ func (s *Session) Open() (err error) {
|
||||||
// lock.
|
// lock.
|
||||||
s.listening = make(chan interface{})
|
s.listening = make(chan interface{})
|
||||||
go s.listen(s.wsConn, s.listening)
|
go s.listen(s.wsConn, s.listening)
|
||||||
|
s.LastHeartbeatAck = time.Now().UTC()
|
||||||
|
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
|
|
||||||
|
@ -187,10 +200,13 @@ type helloOp struct {
|
||||||
Trace []string `json:"_trace"`
|
Trace []string `json:"_trace"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Number of heartbeat intervals to wait until forcing a connection restart.
|
||||||
|
const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond
|
||||||
|
|
||||||
// heartbeat sends regular heartbeats to Discord so it knows the client
|
// heartbeat sends regular heartbeats to Discord so it knows the client
|
||||||
// is still connected. If you do not send these heartbeats Discord will
|
// is still connected. If you do not send these heartbeats Discord will
|
||||||
// disconnect the websocket connection after a few seconds.
|
// disconnect the websocket connection after a few seconds.
|
||||||
func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) {
|
func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, heartbeatIntervalMsec time.Duration) {
|
||||||
|
|
||||||
s.log(LogInformational, "called")
|
s.log(LogInformational, "called")
|
||||||
|
|
||||||
|
@ -199,20 +215,26 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
ticker := time.NewTicker(i * time.Millisecond)
|
ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
s.RLock()
|
||||||
|
last := s.LastHeartbeatAck
|
||||||
|
s.RUnlock()
|
||||||
sequence := atomic.LoadInt64(s.sequence)
|
sequence := atomic.LoadInt64(s.sequence)
|
||||||
s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence)
|
s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence)
|
||||||
s.wsMutex.Lock()
|
s.wsMutex.Lock()
|
||||||
err = wsConn.WriteJSON(heartbeatOp{1, sequence})
|
err = wsConn.WriteJSON(heartbeatOp{1, sequence})
|
||||||
s.wsMutex.Unlock()
|
s.wsMutex.Unlock()
|
||||||
if err != nil {
|
if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) {
|
||||||
s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err)
|
if err != nil {
|
||||||
s.Lock()
|
s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err)
|
||||||
s.DataReady = false
|
} else {
|
||||||
s.Unlock()
|
s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last))
|
||||||
|
}
|
||||||
|
s.Close()
|
||||||
|
s.reconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.Lock()
|
s.Lock()
|
||||||
|
@ -250,7 +272,7 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
if s.wsConn == nil {
|
if s.wsConn == nil {
|
||||||
return errors.New("no websocket connection exists")
|
return ErrWSNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
var usd updateStatusData
|
var usd updateStatusData
|
||||||
|
@ -307,7 +329,7 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
if s.wsConn == nil {
|
if s.wsConn == nil {
|
||||||
return errors.New("no websocket connection exists")
|
return ErrWSNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
data := requestGuildMembersData{
|
data := requestGuildMembersData{
|
||||||
|
@ -386,7 +408,10 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||||
// Reconnect
|
// Reconnect
|
||||||
// Must immediately disconnect from gateway and reconnect to new gateway.
|
// Must immediately disconnect from gateway and reconnect to new gateway.
|
||||||
if e.Operation == 7 {
|
if e.Operation == 7 {
|
||||||
// TODO
|
s.log(LogInformational, "Closing and reconnecting in response to Op7")
|
||||||
|
s.Close()
|
||||||
|
s.reconnect()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid Session
|
// Invalid Session
|
||||||
|
@ -414,6 +439,14 @@ func (s *Session) onEvent(messageType int, message []byte) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.Operation == 11 {
|
||||||
|
s.Lock()
|
||||||
|
s.LastHeartbeatAck = time.Now().UTC()
|
||||||
|
s.Unlock()
|
||||||
|
s.log(LogInformational, "got heartbeat ACK")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Do not try to Dispatch a non-Dispatch Message
|
// Do not try to Dispatch a non-Dispatch Message
|
||||||
if e.Operation != 0 {
|
if e.Operation != 0 {
|
||||||
// But we probably should be doing something with them.
|
// But we probably should be doing something with them.
|
||||||
|
@ -621,7 +654,7 @@ func (s *Session) identify() error {
|
||||||
if s.ShardCount > 1 {
|
if s.ShardCount > 1 {
|
||||||
|
|
||||||
if s.ShardID >= s.ShardCount {
|
if s.ShardID >= s.ShardCount {
|
||||||
return errors.New("ShardID must be less than ShardCount")
|
return ErrWSShardBounds
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Shard = &[2]int{s.ShardID, s.ShardCount}
|
data.Shard = &[2]int{s.ShardID, s.ShardCount}
|
||||||
|
@ -676,6 +709,13 @@ func (s *Session) reconnect() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Certain race conditions can call reconnect() twice. If this happens, we
|
||||||
|
// just break out of the reconnect loop
|
||||||
|
if err == ErrWSAlreadyOpen {
|
||||||
|
s.log(LogInformational, "Websocket already exists, no need to reconnect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.log(LogError, "error reconnecting to gateway, %s", err)
|
s.log(LogError, "error reconnecting to gateway, %s", err)
|
||||||
|
|
||||||
<-time.After(wait * time.Second)
|
<-time.After(wait * time.Second)
|
||||||
|
|
Loading…
Reference in a new issue