Merge branch 'develop' into 1

This commit is contained in:
LEGOlord208 2017-07-23 08:27:41 +02:00
parent 27db9ad6df
commit b813c5d0ca
No known key found for this signature in database
GPG key ID: 3A818BC1F5617A24
14 changed files with 357 additions and 102 deletions

View file

@ -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 ./...

View file

@ -90,7 +90,6 @@ that information in a nice format.
- [![GoDoc](https://godoc.org/github.com/bwmarrin/discordgo?status.svg)](https://godoc.org/github.com/bwmarrin/discordgo) - [![GoDoc](https://godoc.org/github.com/bwmarrin/discordgo?status.svg)](https://godoc.org/github.com/bwmarrin/discordgo)
- [![Go Walker](http://gowalker.org/api/v1/badge)](https://gowalker.org/github.com/bwmarrin/discordgo) - [![Go Walker](http://gowalker.org/api/v1/badge)](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.

View file

@ -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.

View file

@ -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
} }

View file

@ -24,6 +24,7 @@ var (
EndpointChannels = EndpointAPI + "channels/" EndpointChannels = EndpointAPI + "channels/"
EndpointUsers = EndpointAPI + "users/" EndpointUsers = EndpointAPI + "users/"
EndpointGateway = EndpointAPI + "gateway" EndpointGateway = EndpointAPI + "gateway"
EndpointGatewayBot = EndpointGateway + "/bot"
EndpointWebhooks = EndpointAPI + "webhooks/" EndpointWebhooks = EndpointAPI + "webhooks/"
EndpointCDN = "https://cdn.discordapp.com/" EndpointCDN = "https://cdn.discordapp.com/"
@ -56,6 +57,7 @@ var (
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" }
EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" }
EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" }
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }

View file

@ -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.
@ -34,6 +35,7 @@ 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
ContentType string
Reader io.Reader Reader io.Reader
} }
@ -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
View 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)
}
}

View file

@ -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.

View file

@ -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 {

View file

@ -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.

View file

@ -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
View file

@ -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
}

View file

@ -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
}
} }
} }
} }

View file

@ -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 || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) {
if err != nil { if err != nil {
s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err)
s.Lock() } else {
s.DataReady = false s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last))
s.Unlock() }
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)