Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Gregory DALMAR 2019-02-01 15:00:32 +01:00
commit 624ff560d4
19 changed files with 852 additions and 201 deletions

View file

@ -1,12 +1,12 @@
language: go language: go
go: go:
- 1.7.x
- 1.8.x
- 1.9.x - 1.9.x
- 1.10.x
- 1.11.x
install: install:
- go get github.com/bwmarrin/discordgo - go get github.com/bwmarrin/discordgo
- go get -v . - go get -v .
- go get -v github.com/golang/lint/golint - go get -v golang.org/x/lint/golint
script: script:
- diff <(gofmt -d .) <(echo -n) - diff <(gofmt -d .) <(echo -n)
- go vet -x ./... - go vet -x ./...

View file

@ -15,11 +15,11 @@ to add the official DiscordGo test bot **dgo** to your server. This provides
indispensable help to this project. indispensable help to this project.
* See [dgVoice](https://github.com/bwmarrin/dgvoice) package for an example of * See [dgVoice](https://github.com/bwmarrin/dgvoice) package for an example of
additional voice helper functions and features for DiscordGo additional voice helper functions and features for DiscordGo.
* See [dca](https://github.com/bwmarrin/dca) for an **experimental** stand alone * See [dca](https://github.com/bwmarrin/dca) for an **experimental** stand alone
tool that wraps `ffmpeg` to create opus encoded audio appropriate for use with tool that wraps `ffmpeg` to create opus encoded audio appropriate for use with
Discord (and DiscordGo) Discord (and DiscordGo).
**For help with this package or general Go discussion, please join the [Discord **For help with this package or general Go discussion, please join the [Discord
Gophers](https://discord.gg/0f1SbxBZjYq9jLBk) chat server.** Gophers](https://discord.gg/0f1SbxBZjYq9jLBk) chat server.**
@ -39,9 +39,9 @@ the breaking changes get documented before pushing to master.
*So, what should you use?* *So, what should you use?*
If you can accept the constant changing nature of *develop* then it is the If you can accept the constant changing nature of *develop*, it is the
recommended branch to use. Otherwise, if you want to tail behind development recommended branch to use. Otherwise, if you want to tail behind development
slightly and have a more stable package with documented releases then use *master* slightly and have a more stable package with documented releases, use *master*.
### Installing ### Installing
@ -96,10 +96,10 @@ that information in a nice format.
## Examples ## Examples
Below is a list of examples and other projects using DiscordGo. Please submit Below is a list of examples and other projects using DiscordGo. Please submit
an issue if you would like your project added or removed from this list an issue if you would like your project added or removed from this list.
- [DiscordGo Examples](https://github.com/bwmarrin/discordgo/tree/master/examples) A collection of example programs written with DiscordGo - [DiscordGo Examples](https://github.com/bwmarrin/discordgo/tree/master/examples) - A collection of example programs written with DiscordGo
- [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) A curated list of high quality projects using DiscordGo - [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) - A curated list of high quality projects using DiscordGo
## Troubleshooting ## Troubleshooting
For help with common problems please reference the For help with common problems please reference the
@ -114,7 +114,7 @@ Contributions are very welcomed, however please follow the below guidelines.
discussed. discussed.
- Fork the develop branch and make your changes. - Fork the develop branch and make your changes.
- Try to match current naming conventions as closely as possible. - Try to match current naming conventions as closely as possible.
- This package is intended to be a low level direct mapping of the Discord API - This package is intended to be a low level direct mapping of the Discord API,
so please avoid adding enhancements outside of that scope without first so please avoid adding enhancements outside of that scope without first
discussing it. discussing it.
- Create a Pull Request with your changes against the develop branch. - Create a Pull Request with your changes against the develop branch.
@ -127,4 +127,4 @@ comparison and list of other Discord API libraries.
## Special Thanks ## Special Thanks
[Chris Rhodes](https://github.com/iopred) - For the DiscordGo logo and tons of PRs [Chris Rhodes](https://github.com/iopred) - For the DiscordGo logo and tons of PRs.

View file

@ -6,8 +6,8 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// This file contains high level helper functions and easy entry points for the // This file contains high level helper functions and easy entry points for the
// entire discordgo package. These functions are beling developed and are very // entire discordgo package. These functions are being developed and are very
// experimental at this point. They will most likley change so please use the // experimental at this point. They will most likely change so please use the
// low level functions if that's a problem. // low level functions if that's a problem.
// Package discordgo provides Discord binding for Go // Package discordgo provides Discord binding for Go
@ -21,7 +21,7 @@ import (
) )
// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
const VERSION = "0.18.0" const VERSION = "0.19.0"
// 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")

View file

@ -11,6 +11,8 @@
package discordgo package discordgo
import "strconv"
// APIVersion is the Discord API version used for the REST and Websocket API. // APIVersion is the Discord API version used for the REST and Websocket API.
var APIVersion = "6" var APIVersion = "6"
@ -61,14 +63,18 @@ 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" } EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" }
EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } EndpointDefaultUserAvatar = func(uDiscriminator string) string {
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } uDiscriminatorInt, _ := strconv.Atoi(uDiscriminator)
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } return EndpointCDN + "embed/avatars/" + strconv.Itoa(uDiscriminatorInt%5) + ".png"
EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } }
EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" }
EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" }
EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" }
EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" }
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 }
EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" } EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" }
@ -88,6 +94,9 @@ var (
EndpointGuildIcon = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" } EndpointGuildIcon = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" }
EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" } EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" }
EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" }
EndpointGuildAuditLogs = func(gID string) string { return EndpointGuilds + gID + "/audit-logs" }
EndpointGuildEmojis = func(gID string) string { return EndpointGuilds + gID + "/emojis" }
EndpointGuildEmoji = func(gID, eID string) string { return EndpointGuilds + gID + "/emojis/" + eID }
EndpointChannel = func(cID string) string { return EndpointChannels + cID } EndpointChannel = func(cID string) string { return EndpointChannels + cID }
EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" }
@ -127,7 +136,8 @@ var (
EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" }
EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" } EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" }
EndpointEmojiAnimated = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".gif" }
EndpointOauth2 = EndpointAPI + "oauth2/" EndpointOauth2 = EndpointAPI + "oauth2/"
EndpointApplications = EndpointOauth2 + "applications" EndpointApplications = EndpointOauth2 + "applications"

View file

@ -98,7 +98,9 @@ func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() {
// AddHandler allows you to add an event handler that will be fired anytime // AddHandler allows you to add an event handler that will be fired anytime
// the Discord WSAPI event that matches the function fires. // the Discord WSAPI event that matches the function fires.
// events.go contains all the Discord WSAPI events that can be fired. // The first parameter is a *Session, and the second parameter is a pointer
// to a struct corresponding to the event for which you want to listen.
//
// eg: // eg:
// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { // Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
// }) // })
@ -106,6 +108,13 @@ func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() {
// or: // or:
// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { // Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) {
// }) // })
//
// List of events can be found at this page, with corresponding names in the
// library for each event: https://discordapp.com/developers/docs/topics/gateway#event-names
// There are also synthetic events fired by the library internally which are
// available for handling, like Connect, Disconnect, and RateLimit.
// events.go contains all of the Discord WSAPI and synthetic events that can be handled.
//
// The return value of this method is a function, that when called will remove the // The return value of this method is a function, that when called will remove the
// event handler. // event handler.
func (s *Session) AddHandler(handler interface{}) func() { func (s *Session) AddHandler(handler interface{}) func() {

View file

@ -50,6 +50,7 @@ const (
userUpdateEventType = "USER_UPDATE" userUpdateEventType = "USER_UPDATE"
voiceServerUpdateEventType = "VOICE_SERVER_UPDATE" voiceServerUpdateEventType = "VOICE_SERVER_UPDATE"
voiceStateUpdateEventType = "VOICE_STATE_UPDATE" voiceStateUpdateEventType = "VOICE_STATE_UPDATE"
webhooksUpdateEventType = "WEBHOOKS_UPDATE"
) )
// channelCreateEventHandler is an event handler for ChannelCreate events. // channelCreateEventHandler is an event handler for ChannelCreate events.
@ -892,6 +893,26 @@ func (eh voiceStateUpdateEventHandler) Handle(s *Session, i interface{}) {
} }
} }
// webhooksUpdateEventHandler is an event handler for WebhooksUpdate events.
type webhooksUpdateEventHandler func(*Session, *WebhooksUpdate)
// Type returns the event type for WebhooksUpdate events.
func (eh webhooksUpdateEventHandler) Type() string {
return webhooksUpdateEventType
}
// New returns a new instance of WebhooksUpdate.
func (eh webhooksUpdateEventHandler) New() interface{} {
return &WebhooksUpdate{}
}
// Handle is the handler for WebhooksUpdate events.
func (eh webhooksUpdateEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*WebhooksUpdate); ok {
eh(s, t)
}
}
func handlerForInterface(handler interface{}) EventHandler { func handlerForInterface(handler interface{}) EventHandler {
switch v := handler.(type) { switch v := handler.(type) {
case func(*Session, interface{}): case func(*Session, interface{}):
@ -982,6 +1003,8 @@ func handlerForInterface(handler interface{}) EventHandler {
return voiceServerUpdateEventHandler(v) return voiceServerUpdateEventHandler(v)
case func(*Session, *VoiceStateUpdate): case func(*Session, *VoiceStateUpdate):
return voiceStateUpdateEventHandler(v) return voiceStateUpdateEventHandler(v)
case func(*Session, *WebhooksUpdate):
return webhooksUpdateEventHandler(v)
} }
return nil return nil
@ -1027,4 +1050,5 @@ func init() {
registerInterfaceProvider(userUpdateEventHandler(nil)) registerInterfaceProvider(userUpdateEventHandler(nil))
registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) registerInterfaceProvider(voiceServerUpdateEventHandler(nil))
registerInterfaceProvider(voiceStateUpdateEventHandler(nil)) registerInterfaceProvider(voiceStateUpdateEventHandler(nil))
registerInterfaceProvider(webhooksUpdateEventHandler(nil))
} }

View file

@ -70,6 +70,7 @@ type ChannelDelete struct {
type ChannelPinsUpdate struct { type ChannelPinsUpdate struct {
LastPinTimestamp string `json:"last_pin_timestamp"` LastPinTimestamp string `json:"last_pin_timestamp"`
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
GuildID string `json:"guild_id,omitempty"`
} }
// GuildCreate is the data for a GuildCreate event. // GuildCreate is the data for a GuildCreate event.
@ -212,6 +213,7 @@ type RelationshipRemove struct {
type TypingStart struct { type TypingStart struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
GuildID string `json:"guild_id,omitempty"`
Timestamp int `json:"timestamp"` Timestamp int `json:"timestamp"`
} }
@ -250,4 +252,11 @@ type VoiceStateUpdate struct {
type MessageDeleteBulk struct { type MessageDeleteBulk struct {
Messages []string `json:"ids"` Messages []string `json:"ids"`
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
GuildID string `json:"guild_id"`
}
// WebhooksUpdate is the data for a WebhooksUpdate event
type WebhooksUpdate struct {
GuildID string `json:"guild_id"`
ChannelID string `json:"channel_id"`
} }

6
go.mod Normal file
View file

@ -0,0 +1,6 @@
module github.com/bwmarrin/discordgo
require (
github.com/gorilla/websocket v1.4.0
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16
)

4
go.sum Normal file
View file

@ -0,0 +1,4 @@
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

View file

@ -32,20 +32,59 @@ const (
// A Message stores all data related to a specific Discord message. // A Message stores all data related to a specific Discord message.
type Message struct { type Message struct {
ID string `json:"id"` // The ID of the message.
ChannelID string `json:"channel_id"` ID string `json:"id"`
Content string `json:"content"`
Timestamp Timestamp `json:"timestamp"` // The ID of the channel in which the message was sent.
EditedTimestamp Timestamp `json:"edited_timestamp"` ChannelID string `json:"channel_id"`
MentionRoles []string `json:"mention_roles"`
Tts bool `json:"tts"` // The ID of the guild in which the message was sent.
MentionEveryone bool `json:"mention_everyone"` GuildID string `json:"guild_id,omitempty"`
Author *User `json:"author"`
Attachments []*MessageAttachment `json:"attachments"` // The content of the message.
Embeds []*MessageEmbed `json:"embeds"` Content string `json:"content"`
Mentions []*User `json:"mentions"`
Reactions []*MessageReactions `json:"reactions"` // The time at which the messsage was sent.
Type MessageType `json:"type"` // CAUTION: this field may be removed in a
// future API version; it is safer to calculate
// the creation time via the ID.
Timestamp Timestamp `json:"timestamp"`
// The time at which the last edit of the message
// occurred, if it has been edited.
EditedTimestamp Timestamp `json:"edited_timestamp"`
// The roles mentioned in the message.
MentionRoles []string `json:"mention_roles"`
// Whether the message is text-to-speech.
Tts bool `json:"tts"`
// Whether the message mentions everyone.
MentionEveryone bool `json:"mention_everyone"`
// The author of the message. This is not guaranteed to be a
// valid user (webhook-sent messages do not possess a full author).
Author *User `json:"author"`
// A list of attachments present in the message.
Attachments []*MessageAttachment `json:"attachments"`
// A list of embeds present in the message. Multiple
// embeds can currently only be sent by webhooks.
Embeds []*MessageEmbed `json:"embeds"`
// A list of users mentioned in the message.
Mentions []*User `json:"mentions"`
// A list of reactions to the message.
Reactions []*MessageReactions `json:"reactions"`
// The type of the message.
Type MessageType `json:"type"`
// The webhook ID of the message, if it was generated by a webhook
WebhookID string `json:"webhook_id"`
} }
// File stores info about files you e.g. send in messages. // File stores info about files you e.g. send in messages.
@ -237,7 +276,7 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e
continue continue
} }
content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1) content = strings.Replace(content, "<@&"+role.ID+">", "@"+role.Name, -1)
} }
content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string {

View file

@ -12,7 +12,6 @@ func TestContentWithMoreMentionsReplaced(t *testing.T) {
Username: "User Name", Username: "User Name",
} }
s.StateEnabled = true
s.State.GuildAdd(&Guild{ID: "guild"}) s.State.GuildAdd(&Guild{ID: "guild"})
s.State.RoleAdd("guild", &Role{ s.State.RoleAdd("guild", &Role{
ID: "role", ID: "role",
@ -30,7 +29,7 @@ func TestContentWithMoreMentionsReplaced(t *testing.T) {
ID: "channel", ID: "channel",
}) })
m := &Message{ m := &Message{
Content: "<&role> <@!user> <@user> <#channel>", Content: "<@&role> <@!user> <@user> <#channel>",
ChannelID: "channel", ChannelID: "channel",
MentionRoles: []string{"role"}, MentionRoles: []string{"role"},
Mentions: []*User{user}, Mentions: []*User{user},

View file

@ -9,7 +9,7 @@ import (
func ExampleApplication() { func ExampleApplication() {
// Authentication Token pulled from environment variable DG_TOKEN // Authentication Token pulled from environment variable DGU_TOKEN
Token := os.Getenv("DGU_TOKEN") Token := os.Getenv("DGU_TOKEN")
if Token == "" { if Token == "" {
return return

View file

@ -38,6 +38,7 @@ var (
ErrPruneDaysBounds = errors.New("the number of days should be more than or equal to 1") 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") ErrGuildNoIcon = errors.New("guild does not have an icon set")
ErrGuildNoSplash = errors.New("guild does not have a splash set") ErrGuildNoSplash = errors.New("guild does not have a splash set")
ErrUnauthorized = errors.New("HTTP request was unauthorized. This could be because the provided token was not a bot token. Please add \"Bot \" to the start of your token. https://discordapp.com/developers/docs/reference#authentication-example-bot-token-authorization-header")
) )
// 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
@ -89,7 +90,7 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
req.Header.Set("Content-Type", contentType) req.Header.Set("Content-Type", contentType)
// TODO: Make a configurable static variable. // TODO: Make a configurable static variable.
req.Header.Set("User-Agent", fmt.Sprintf("DiscordBot (https://github.com/bwmarrin/discordgo, v%s)", VERSION)) req.Header.Set("User-Agent", "DiscordBot (https://github.com/bwmarrin/discordgo, v"+VERSION+")")
if s.Debug { if s.Debug {
for k, v := range req.Header { for k, v := range req.Header {
@ -129,13 +130,9 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
} }
switch resp.StatusCode { switch resp.StatusCode {
case http.StatusOK: case http.StatusOK:
case http.StatusCreated: case http.StatusCreated:
case http.StatusNoContent: case http.StatusNoContent:
// TODO check for 401 response, invalidate token if we get one.
case http.StatusBadGateway: case http.StatusBadGateway:
// Retry sending request if possible // Retry sending request if possible
if sequence < s.MaxRestRetries { if sequence < s.MaxRestRetries {
@ -145,7 +142,6 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
} else { } else {
err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response)
} }
case 429: // TOO MANY REQUESTS - Rate limiting case 429: // TOO MANY REQUESTS - Rate limiting
rl := TooManyRequests{} rl := TooManyRequests{}
err = json.Unmarshal(response, &rl) err = json.Unmarshal(response, &rl)
@ -161,7 +157,12 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
// this method can cause longer delays than required // this method can cause longer delays than required
response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence) response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence)
case http.StatusUnauthorized:
if strings.Index(s.Token, "Bot ") != 0 {
s.log(LogInformational, ErrUnauthorized.Error())
err = ErrUnauthorized
}
fallthrough
default: // Error condition default: // Error condition
err = newRestError(req, resp, response) err = newRestError(req, resp, response)
} }
@ -249,7 +250,7 @@ func (s *Session) Register(username string) (token string, err error) {
// even use. // even use.
func (s *Session) Logout() (err error) { func (s *Session) Logout() (err error) {
// _, err = s.Request("POST", LOGOUT, fmt.Sprintf(`{"token": "%s"}`, s.Token)) // _, err = s.Request("POST", LOGOUT, `{"token": "` + s.Token + `"}`)
if s.Token == "" { if s.Token == "" {
return return
@ -361,6 +362,21 @@ func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) {
return return
} }
// UserConnections returns the user's connections
func (s *Session) UserConnections() (conn []*UserConnection, err error) {
response, err := s.RequestWithBucketID("GET", EndpointUserConnections("@me"), nil, EndpointUserConnections("@me"))
if err != nil {
return nil, err
}
err = unmarshal(response, &conn)
if err != nil {
return
}
return
}
// UserChannels returns an array of Channel structures for all private // UserChannels returns an array of Channel structures for all private
// channels. // channels.
func (s *Session) UserChannels() (st []*Channel, err error) { func (s *Session) UserChannels() (st []*Channel, err error) {
@ -412,7 +428,7 @@ func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGui
uri := EndpointUserGuilds("@me") uri := EndpointUserGuilds("@me")
if len(v) > 0 { if len(v) > 0 {
uri = fmt.Sprintf("%s?%s", uri, v.Encode()) uri += "?" + v.Encode()
} }
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointUserGuilds("")) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointUserGuilds(""))
@ -565,7 +581,7 @@ func (s *Session) Guild(guildID string) (st *Guild, err error) {
if s.StateEnabled { if s.StateEnabled {
// Attempt to grab the guild from State first. // Attempt to grab the guild from State first.
st, err = s.State.Guild(guildID) st, err = s.State.Guild(guildID)
if err == nil { if err == nil && !st.Unavailable {
return return
} }
} }
@ -735,7 +751,7 @@ func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*M
} }
if len(v) > 0 { if len(v) > 0 {
uri = fmt.Sprintf("%s?%s", uri, v.Encode()) uri += "?" + v.Encode()
} }
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildMembers(guildID)) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildMembers(guildID))
@ -761,6 +777,32 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) {
return return
} }
// GuildMemberAdd force joins a user to the guild.
// accessToken : Valid access_token for the user.
// guildID : The ID of a Guild.
// userID : The ID of a User.
// nick : Value to set users nickname to
// roles : A list of role ID's to set on the member.
// mute : If the user is muted.
// deaf : If the user is deafened.
func (s *Session) GuildMemberAdd(accessToken, guildID, userID, nick string, roles []string, mute, deaf bool) (err error) {
data := struct {
AccessToken string `json:"access_token"`
Nick string `json:"nick,omitempty"`
Roles []string `json:"roles,omitempty"`
Mute bool `json:"mute,omitempty"`
Deaf bool `json:"deaf,omitempty"`
}{accessToken, nick, roles, mute, deaf}
_, err = s.RequestWithBucketID("PUT", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, ""))
if err != nil {
return err
}
return err
}
// GuildMemberDelete removes the given user from the given guild. // GuildMemberDelete removes the given user from the given guild.
// guildID : The ID of a Guild. // guildID : The ID of a Guild.
// userID : The ID of a User // userID : The ID of a User
@ -877,17 +919,22 @@ func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) {
return return
} }
// GuildChannelCreate creates a new channel in the given guild // GuildChannelCreateData is provided to GuildChannelCreateComplex
// guildID : The ID of a Guild. type GuildChannelCreateData struct {
// name : Name of the channel (2-100 chars length) Name string `json:"name"`
// ctype : Tpye of the channel (voice or text) Type ChannelType `json:"type"`
func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, err error) { Topic string `json:"topic,omitempty"`
Bitrate int `json:"bitrate,omitempty"`
data := struct { UserLimit int `json:"user_limit,omitempty"`
Name string `json:"name"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
Type string `json:"type"` ParentID string `json:"parent_id,omitempty"`
}{name, ctype} NSFW bool `json:"nsfw,omitempty"`
}
// GuildChannelCreateComplex creates a new channel in the given guild
// guildID : The ID of a Guild
// data : A data struct describing the new Channel, Name and Type are mandatory, other fields depending on the type
func (s *Session) GuildChannelCreateComplex(guildID string, data GuildChannelCreateData) (st *Channel, err error) {
body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID))
if err != nil { if err != nil {
return return
@ -897,12 +944,33 @@ func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel,
return return
} }
// GuildChannelCreate creates a new channel in the given guild
// guildID : The ID of a Guild.
// name : Name of the channel (2-100 chars length)
// ctype : Type of the channel
func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (st *Channel, err error) {
return s.GuildChannelCreateComplex(guildID, GuildChannelCreateData{
Name: name,
Type: ctype,
})
}
// GuildChannelsReorder updates the order of channels in a guild // GuildChannelsReorder updates the order of channels in a guild
// guildID : The ID of a Guild. // guildID : The ID of a Guild.
// channels : Updated channels. // channels : Updated channels.
func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) { func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) {
_, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), channels, EndpointGuildChannels(guildID)) data := make([]struct {
ID string `json:"id"`
Position int `json:"position"`
}, len(channels))
for i, c := range channels {
data[i].ID = c.ID
data[i].Position = c.Position
}
_, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID))
return return
} }
@ -1021,7 +1089,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er
Pruned uint32 `json:"pruned"` Pruned uint32 `json:"pruned"`
}{} }{}
uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days) uri := EndpointGuildPrune(guildID) + "?days=" + strconv.FormatUint(uint64(days), 10)
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID))
if err != nil { if err != nil {
return return
@ -1075,7 +1143,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err
// GuildIntegrations returns an array of Integrations for a guild. // GuildIntegrations returns an array of Integrations for a guild.
// guildID : The ID of a Guild. // guildID : The ID of a Guild.
func (s *Session) GuildIntegrations(guildID string) (st []*GuildIntegration, err error) { func (s *Session) GuildIntegrations(guildID string) (st []*Integration, err error) {
body, err := s.RequestWithBucketID("GET", EndpointGuildIntegrations(guildID), nil, EndpointGuildIntegrations(guildID)) body, err := s.RequestWithBucketID("GET", EndpointGuildIntegrations(guildID), nil, EndpointGuildIntegrations(guildID))
if err != nil { if err != nil {
@ -1206,6 +1274,94 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string)
return return
} }
// GuildAuditLog returns the audit log for a Guild.
// guildID : The ID of a Guild.
// userID : If provided the log will be filtered for the given ID.
// beforeID : If provided all log entries returned will be before the given ID.
// actionType : If provided the log will be filtered for the given Action Type.
// limit : The number messages that can be returned. (default 50, min 1, max 100)
func (s *Session) GuildAuditLog(guildID, userID, beforeID string, actionType, limit int) (st *GuildAuditLog, err error) {
uri := EndpointGuildAuditLogs(guildID)
v := url.Values{}
if userID != "" {
v.Set("user_id", userID)
}
if beforeID != "" {
v.Set("before", beforeID)
}
if actionType > 0 {
v.Set("action_type", strconv.Itoa(actionType))
}
if limit > 0 {
v.Set("limit", strconv.Itoa(limit))
}
if len(v) > 0 {
uri = fmt.Sprintf("%s?%s", uri, v.Encode())
}
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildAuditLogs(guildID))
if err != nil {
return
}
err = unmarshal(body, &st)
return
}
// GuildEmojiCreate creates a new emoji
// guildID : The ID of a Guild.
// name : The Name of the Emoji.
// image : The base64 encoded emoji image, has to be smaller than 256KB.
// roles : The roles for which this emoji will be whitelisted, can be nil.
func (s *Session) GuildEmojiCreate(guildID, name, image string, roles []string) (emoji *Emoji, err error) {
data := struct {
Name string `json:"name"`
Image string `json:"image"`
Roles []string `json:"roles,omitempty"`
}{name, image, roles}
body, err := s.RequestWithBucketID("POST", EndpointGuildEmojis(guildID), data, EndpointGuildEmojis(guildID))
if err != nil {
return
}
err = unmarshal(body, &emoji)
return
}
// GuildEmojiEdit modifies an emoji
// guildID : The ID of a Guild.
// emojiID : The ID of an Emoji.
// name : The Name of the Emoji.
// roles : The roles for which this emoji will be whitelisted, can be nil.
func (s *Session) GuildEmojiEdit(guildID, emojiID, name string, roles []string) (emoji *Emoji, err error) {
data := struct {
Name string `json:"name"`
Roles []string `json:"roles,omitempty"`
}{name, roles}
body, err := s.RequestWithBucketID("PATCH", EndpointGuildEmoji(guildID, emojiID), data, EndpointGuildEmojis(guildID))
if err != nil {
return
}
err = unmarshal(body, &emoji)
return
}
// GuildEmojiDelete deletes an Emoji.
// guildID : The ID of a Guild.
// emojiID : The ID of an Emoji.
func (s *Session) GuildEmojiDelete(guildID, emojiID string) (err error) {
_, err = s.RequestWithBucketID("DELETE", EndpointGuildEmoji(guildID, emojiID), nil, EndpointGuildEmojis(guildID))
return
}
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
// Functions specific to Discord Channels // Functions specific to Discord Channels
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
@ -1291,7 +1447,7 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID
v.Set("around", aroundID) v.Set("around", aroundID)
} }
if len(v) > 0 { if len(v) > 0 {
uri = fmt.Sprintf("%s?%s", uri, v.Encode()) uri += "?" + v.Encode()
} }
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointChannelMessages(channelID)) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointChannelMessages(channelID))
@ -1586,7 +1742,8 @@ func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, e
MaxAge int `json:"max_age"` MaxAge int `json:"max_age"`
MaxUses int `json:"max_uses"` MaxUses int `json:"max_uses"`
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
}{i.MaxAge, i.MaxUses, i.Temporary} Unique bool `json:"unique"`
}{i.MaxAge, i.MaxUses, i.Temporary, i.Unique}
body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID))
if err != nil { if err != nil {
@ -1638,6 +1795,19 @@ func (s *Session) Invite(inviteID string) (st *Invite, err error) {
return return
} }
// InviteWithCounts returns an Invite structure of the given invite including approximate member counts
// inviteID : The invite code
func (s *Session) InviteWithCounts(inviteID string) (st *Invite, err error) {
body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID)+"?with_counts=true", nil, EndpointInvite(""))
if err != nil {
return
}
err = unmarshal(body, &st)
return
}
// InviteDelete deletes an existing invite // InviteDelete deletes an existing invite
// inviteID : the code of an invite // inviteID : the code of an invite
func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
@ -1830,12 +2000,13 @@ func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err er
// webhookID: The ID of a webhook. // webhookID: The ID of a webhook.
// name : The name of the webhook. // name : The name of the webhook.
// avatar : The avatar of the webhook. // avatar : The avatar of the webhook.
func (s *Session) WebhookEdit(webhookID, name, avatar string) (st *Role, err error) { func (s *Session) WebhookEdit(webhookID, name, avatar, channelID string) (st *Role, err error) {
data := struct { data := struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Avatar string `json:"avatar,omitempty"` Avatar string `json:"avatar,omitempty"`
}{name, avatar} ChannelID string `json:"channel_id,omitempty"`
}{name, avatar, channelID}
body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks) body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks)
if err != nil { if err != nil {
@ -1965,7 +2136,7 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i
} }
if len(v) > 0 { if len(v) > 0 {
uri = fmt.Sprintf("%s?%s", uri, v.Encode()) uri += "?" + v.Encode()
} }
body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", "")) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", ""))

View file

@ -32,6 +32,7 @@ type State struct {
sync.RWMutex sync.RWMutex
Ready Ready
// MaxMessageCount represents how many messages per channel the state will store.
MaxMessageCount int MaxMessageCount int
TrackChannels bool TrackChannels bool
TrackEmojis bool TrackEmojis bool
@ -98,6 +99,9 @@ func (s *State) GuildAdd(guild *Guild) error {
if g, ok := s.guildMap[guild.ID]; ok { if g, ok := s.guildMap[guild.ID]; ok {
// We are about to replace `g` in the state with `guild`, but first we need to // We are about to replace `g` in the state with `guild`, but first we need to
// make sure we preserve any fields that the `guild` doesn't contain from `g`. // make sure we preserve any fields that the `guild` doesn't contain from `g`.
if guild.MemberCount == 0 {
guild.MemberCount = g.MemberCount
}
if guild.Roles == nil { if guild.Roles == nil {
guild.Roles = g.Roles guild.Roles = g.Roles
} }
@ -299,7 +303,12 @@ func (s *State) MemberAdd(member *Member) error {
members[member.User.ID] = member members[member.User.ID] = member
guild.Members = append(guild.Members, member) guild.Members = append(guild.Members, member)
} else { } else {
*m = *member // Update the actual data, which will also update the member pointer in the slice // We are about to replace `m` in the state with `member`, but first we need to
// make sure we preserve any fields that the `member` doesn't contain from `m`.
if member.JoinedAt == "" {
member.JoinedAt = m.JoinedAt
}
*m = *member
} }
return nil return nil
@ -607,7 +616,7 @@ func (s *State) EmojisAdd(guildID string, emojis []*Emoji) error {
// MessageAdd adds a message to the current world state, or updates it if it exists. // MessageAdd adds a message to the current world state, or updates it if it exists.
// If the channel cannot be found, the message is discarded. // If the channel cannot be found, the message is discarded.
// Messages are kept in state up to s.MaxMessageCount // Messages are kept in state up to s.MaxMessageCount per channel.
func (s *State) MessageAdd(message *Message) error { func (s *State) MessageAdd(message *Message) error {
if s == nil { if s == nil {
return ErrNilState return ErrNilState
@ -805,6 +814,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
case *GuildDelete: case *GuildDelete:
err = s.GuildRemove(t.Guild) err = s.GuildRemove(t.Guild)
case *GuildMemberAdd: case *GuildMemberAdd:
// Updates the MemberCount of the guild.
guild, err := s.Guild(t.Member.GuildID)
if err != nil {
return err
}
guild.MemberCount++
// Caches member if tracking is enabled.
if s.TrackMembers { if s.TrackMembers {
err = s.MemberAdd(t.Member) err = s.MemberAdd(t.Member)
} }
@ -813,6 +830,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
err = s.MemberAdd(t.Member) err = s.MemberAdd(t.Member)
} }
case *GuildMemberRemove: case *GuildMemberRemove:
// Updates the MemberCount of the guild.
guild, err := s.Guild(t.Member.GuildID)
if err != nil {
return err
}
guild.MemberCount--
// Removes member from the cache if tracking is enabled.
if s.TrackMembers { if s.TrackMembers {
err = s.MemberRemove(t.Member) err = s.MemberRemove(t.Member)
} }

View file

@ -13,6 +13,7 @@ package discordgo
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"sync" "sync"
"time" "time"
@ -84,6 +85,9 @@ type Session struct {
// Stores the last HeartbeatAck that was recieved (in UTC) // Stores the last HeartbeatAck that was recieved (in UTC)
LastHeartbeatAck time.Time LastHeartbeatAck time.Time
// Stores the last Heartbeat sent (in UTC)
LastHeartbeatSent time.Time
// used to deal with rate limits // used to deal with rate limits
Ratelimiter *RateLimiter Ratelimiter *RateLimiter
@ -111,6 +115,37 @@ type Session struct {
wsMutex sync.Mutex wsMutex sync.Mutex
} }
// UserConnection is a Connection returned from the UserConnections endpoint
type UserConnection struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Revoked bool `json:"revoked"`
Integrations []*Integration `json:"integrations"`
}
// Integration stores integration information
type Integration struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
Syncing bool `json:"syncing"`
RoleID string `json:"role_id"`
ExpireBehavior int `json:"expire_behavior"`
ExpireGracePeriod int `json:"expire_grace_period"`
User *User `json:"user"`
Account IntegrationAccount `json:"account"`
SyncedAt Timestamp `json:"synced_at"`
}
// IntegrationAccount is integration account information
// sent by the UserConnections endpoint
type IntegrationAccount struct {
ID string `json:"id"`
Name string `json:"name"`
}
// A VoiceRegion stores data for a specific voice region server. // A VoiceRegion stores data for a specific voice region server.
type VoiceRegion struct { type VoiceRegion struct {
ID string `json:"id"` ID string `json:"id"`
@ -145,6 +180,10 @@ type Invite struct {
Revoked bool `json:"revoked"` Revoked bool `json:"revoked"`
Temporary bool `json:"temporary"` Temporary bool `json:"temporary"`
Unique bool `json:"unique"` Unique bool `json:"unique"`
// will only be filled when using InviteWithCounts
ApproximatePresenceCount int `json:"approximate_presence_count"`
ApproximateMemberCount int `json:"approximate_member_count"`
} }
// ChannelType is the type of a Channel // ChannelType is the type of a Channel
@ -161,22 +200,61 @@ const (
// A Channel holds all data related to an individual Discord channel. // A Channel holds all data related to an individual Discord channel.
type Channel struct { type Channel struct {
ID string `json:"id"` // The ID of the channel.
GuildID string `json:"guild_id"` ID string `json:"id"`
Name string `json:"name"`
Topic string `json:"topic"` // The ID of the guild to which the channel belongs, if it is in a guild.
Type ChannelType `json:"type"` // Else, this ID is empty (e.g. DM channels).
LastMessageID string `json:"last_message_id"` GuildID string `json:"guild_id"`
NSFW bool `json:"nsfw"`
Position int `json:"position"` // The name of the channel.
Bitrate int `json:"bitrate"` Name string `json:"name"`
Recipients []*User `json:"recipients"`
Messages []*Message `json:"-"` // The topic of the channel.
Topic string `json:"topic"`
// The type of the channel.
Type ChannelType `json:"type"`
// The ID of the last message sent in the channel. This is not
// guaranteed to be an ID of a valid message.
LastMessageID string `json:"last_message_id"`
// Whether the channel is marked as NSFW.
NSFW bool `json:"nsfw"`
// Icon of the group DM channel.
Icon string `json:"icon"`
// The position of the channel, used for sorting in client.
Position int `json:"position"`
// The bitrate of the channel, if it is a voice channel.
Bitrate int `json:"bitrate"`
// The recipients of the channel. This is only populated in DM channels.
Recipients []*User `json:"recipients"`
// The messages in the channel. This is only present in state-cached channels,
// and State.MaxMessageCount must be non-zero.
Messages []*Message `json:"-"`
// A list of permission overwrites present for the channel.
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
ParentID string `json:"parent_id"`
// The user limit of the voice channel.
UserLimit int `json:"user_limit"`
// The ID of the parent channel, if the channel is under a category
ParentID string `json:"parent_id"`
} }
// A ChannelEdit holds Channel Feild data for a channel edit. // Mention returns a string which mentions the channel
func (c *Channel) Mention() string {
return fmt.Sprintf("<#%s>", c.ID)
}
// A ChannelEdit holds Channel Field data for a channel edit.
type ChannelEdit struct { type ChannelEdit struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Topic string `json:"topic,omitempty"` Topic string `json:"topic,omitempty"`
@ -186,6 +264,7 @@ type ChannelEdit struct {
UserLimit int `json:"user_limit,omitempty"` UserLimit int `json:"user_limit,omitempty"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
ParentID string `json:"parent_id,omitempty"` ParentID string `json:"parent_id,omitempty"`
RateLimitPerUser int `json:"rate_limit_per_user,omitempty"`
} }
// A PermissionOverwrite holds permission overwrite data for a Channel // A PermissionOverwrite holds permission overwrite data for a Channel
@ -206,6 +285,19 @@ type Emoji struct {
Animated bool `json:"animated"` Animated bool `json:"animated"`
} }
// MessageFormat returns a correctly formatted Emoji for use in Message content and embeds
func (e *Emoji) MessageFormat() string {
if e.ID != "" && e.Name != "" {
if e.Animated {
return "<a:" + e.APIName() + ">"
}
return "<:" + e.APIName() + ">"
}
return e.APIName()
}
// APIName returns an correctly formatted API name for use in the MessageReactions endpoints. // APIName returns an correctly formatted API name for use in the MessageReactions endpoints.
func (e *Emoji) APIName() string { func (e *Emoji) APIName() string {
if e.ID != "" && e.Name != "" { if e.ID != "" && e.Name != "" {
@ -228,31 +320,129 @@ const (
VerificationLevelHigh VerificationLevelHigh
) )
// ExplicitContentFilterLevel type definition
type ExplicitContentFilterLevel int
// Constants for ExplicitContentFilterLevel levels from 0 to 2 inclusive
const (
ExplicitContentFilterDisabled ExplicitContentFilterLevel = iota
ExplicitContentFilterMembersWithoutRoles
ExplicitContentFilterAllMembers
)
// MfaLevel type definition
type MfaLevel int
// Constants for MfaLevel levels from 0 to 1 inclusive
const (
MfaLevelNone MfaLevel = iota
MfaLevelElevated
)
// A Guild holds all data related to a specific Discord Guild. Guilds are also // A Guild holds all data related to a specific Discord Guild. Guilds are also
// sometimes referred to as Servers in the Discord client. // sometimes referred to as Servers in the Discord client.
type Guild struct { type Guild struct {
ID string `json:"id"` // The ID of the guild.
Name string `json:"name"` ID string `json:"id"`
Icon string `json:"icon"`
Region string `json:"region"` // The name of the guild. (2100 characters)
AfkChannelID string `json:"afk_channel_id"` Name string `json:"name"`
EmbedChannelID string `json:"embed_channel_id"`
OwnerID string `json:"owner_id"` // The hash of the guild's icon. Use Session.GuildIcon
JoinedAt Timestamp `json:"joined_at"` // to retrieve the icon itself.
Splash string `json:"splash"` Icon string `json:"icon"`
AfkTimeout int `json:"afk_timeout"`
MemberCount int `json:"member_count"` // The voice region of the guild.
VerificationLevel VerificationLevel `json:"verification_level"` Region string `json:"region"`
EmbedEnabled bool `json:"embed_enabled"`
Large bool `json:"large"` // ?? // The ID of the AFK voice channel.
DefaultMessageNotifications int `json:"default_message_notifications"` AfkChannelID string `json:"afk_channel_id"`
Roles []*Role `json:"roles"`
Emojis []*Emoji `json:"emojis"` // The ID of the embed channel ID, used for embed widgets.
Members []*Member `json:"members"` EmbedChannelID string `json:"embed_channel_id"`
Presences []*Presence `json:"presences"`
Channels []*Channel `json:"channels"` // The user ID of the owner of the guild.
VoiceStates []*VoiceState `json:"voice_states"` OwnerID string `json:"owner_id"`
Unavailable bool `json:"unavailable"`
// The time at which the current user joined the guild.
// This field is only present in GUILD_CREATE events and websocket
// update events, and thus is only present in state-cached guilds.
JoinedAt Timestamp `json:"joined_at"`
// The hash of the guild's splash.
Splash string `json:"splash"`
// The timeout, in seconds, before a user is considered AFK in voice.
AfkTimeout int `json:"afk_timeout"`
// The number of members in the guild.
// This field is only present in GUILD_CREATE events and websocket
// update events, and thus is only present in state-cached guilds.
MemberCount int `json:"member_count"`
// The verification level required for the guild.
VerificationLevel VerificationLevel `json:"verification_level"`
// Whether the guild has embedding enabled.
EmbedEnabled bool `json:"embed_enabled"`
// Whether the guild is considered large. This is
// determined by a member threshold in the identify packet,
// and is currently hard-coded at 250 members in the library.
Large bool `json:"large"`
// The default message notification setting for the guild.
// 0 == all messages, 1 == mentions only.
DefaultMessageNotifications int `json:"default_message_notifications"`
// A list of roles in the guild.
Roles []*Role `json:"roles"`
// A list of the custom emojis present in the guild.
Emojis []*Emoji `json:"emojis"`
// A list of the members in the guild.
// This field is only present in GUILD_CREATE events and websocket
// update events, and thus is only present in state-cached guilds.
Members []*Member `json:"members"`
// A list of partial presence objects for members in the guild.
// This field is only present in GUILD_CREATE events and websocket
// update events, and thus is only present in state-cached guilds.
Presences []*Presence `json:"presences"`
// A list of channels in the guild.
// This field is only present in GUILD_CREATE events and websocket
// update events, and thus is only present in state-cached guilds.
Channels []*Channel `json:"channels"`
// A list of voice states for the guild.
// This field is only present in GUILD_CREATE events and websocket
// update events, and thus is only present in state-cached guilds.
VoiceStates []*VoiceState `json:"voice_states"`
// Whether this guild is currently unavailable (most likely due to outage).
// This field is only present in GUILD_CREATE events and websocket
// update events, and thus is only present in state-cached guilds.
Unavailable bool `json:"unavailable"`
// The explicit content filter level
ExplicitContentFilter ExplicitContentFilterLevel `json:"explicit_content_filter"`
// The list of enabled guild features
Features []string `json:"features"`
// Required MFA level for the guild
MfaLevel MfaLevel `json:"mfa_level"`
// Whether or not the Server Widget is enabled
WidgetEnabled bool `json:"widget_enabled"`
// The Channel ID for the Server Widget
WidgetChannelID string `json:"widget_channel_id"`
// The Channel ID to which system messages are sent (eg join and leave messages)
SystemChannelID string `json:"system_channel_id"`
} }
// A UserGuild holds a brief version of a Guild // A UserGuild holds a brief version of a Guild
@ -279,14 +469,37 @@ type GuildParams struct {
// A Role stores information about Discord guild member roles. // A Role stores information about Discord guild member roles.
type Role struct { type Role struct {
ID string `json:"id"` // The ID of the role.
Name string `json:"name"` ID string `json:"id"`
Managed bool `json:"managed"`
Mentionable bool `json:"mentionable"` // The name of the role.
Hoist bool `json:"hoist"` Name string `json:"name"`
Color int `json:"color"`
Position int `json:"position"` // Whether this role is managed by an integration, and
Permissions int `json:"permissions"` // thus cannot be manually added to, or taken from, members.
Managed bool `json:"managed"`
// Whether this role is mentionable.
Mentionable bool `json:"mentionable"`
// Whether this role is hoisted (shows up separately in member list).
Hoist bool `json:"hoist"`
// The hex color of this role.
Color int `json:"color"`
// The position of this role in the guild's role hierarchy.
Position int `json:"position"`
// The permissions of the role on the guild (doesn't include channel overrides).
// This is a combination of bit masks; the presence of a certain permission can
// be checked by performing a bitwise AND between this int and the permission.
Permissions int `json:"permissions"`
}
// Mention returns a string which mentions the role
func (r *Role) Mention() string {
return fmt.Sprintf("<@&%s>", r.ID)
} }
// Roles are a collection of Role // Roles are a collection of Role
@ -334,6 +547,8 @@ type GameType int
const ( const (
GameTypeGame GameType = iota GameTypeGame GameType = iota
GameTypeStreaming GameTypeStreaming
GameTypeListening
GameTypeWatching
) )
// A Game struct holds the name of the "playing .." game for a user // A Game struct holds the name of the "playing .." game for a user
@ -379,15 +594,34 @@ type Assets struct {
SmallText string `json:"small_text,omitempty"` SmallText string `json:"small_text,omitempty"`
} }
// A Member stores user information for Guild members. // A Member stores user information for Guild members. A guild
// member represents a certain user's presence in a guild.
type Member struct { type Member struct {
GuildID string `json:"guild_id"` // The guild ID on which the member exists.
JoinedAt string `json:"joined_at"` GuildID string `json:"guild_id"`
Nick string `json:"nick"`
Deaf bool `json:"deaf"` // The time at which the member joined the guild, in ISO8601.
Mute bool `json:"mute"` JoinedAt Timestamp `json:"joined_at"`
User *User `json:"user"`
Roles []string `json:"roles"` // The nickname of the member, if they have one.
Nick string `json:"nick"`
// Whether the member is deafened at a guild level.
Deaf bool `json:"deaf"`
// Whether the member is muted at a guild level.
Mute bool `json:"mute"`
// The underlying user on which the member is based.
User *User `json:"user"`
// A list of IDs of the roles which are possessed by the member.
Roles []string `json:"roles"`
}
// Mention creates a member mention
func (m *Member) Mention() string {
return "<@!" + m.User.ID + ">"
} }
// A Settings stores data for a specific users Discord client settings. // A Settings stores data for a specific users Discord client settings.
@ -467,33 +701,88 @@ type GuildBan struct {
User *User `json:"user"` User *User `json:"user"`
} }
// A GuildIntegration stores data for a guild integration.
type GuildIntegration struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
Syncing bool `json:"syncing"`
RoleID string `json:"role_id"`
ExpireBehavior int `json:"expire_behavior"`
ExpireGracePeriod int `json:"expire_grace_period"`
User *User `json:"user"`
Account *GuildIntegrationAccount `json:"account"`
SyncedAt int `json:"synced_at"`
}
// A GuildIntegrationAccount stores data for a guild integration account.
type GuildIntegrationAccount struct {
ID string `json:"id"`
Name string `json:"name"`
}
// A GuildEmbed stores data for a guild embed. // A GuildEmbed stores data for a guild embed.
type GuildEmbed struct { type GuildEmbed struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
} }
// A GuildAuditLog stores data for a guild audit log.
type GuildAuditLog struct {
Webhooks []struct {
ChannelID string `json:"channel_id"`
GuildID string `json:"guild_id"`
ID string `json:"id"`
Avatar string `json:"avatar"`
Name string `json:"name"`
} `json:"webhooks,omitempty"`
Users []struct {
Username string `json:"username"`
Discriminator string `json:"discriminator"`
Bot bool `json:"bot"`
ID string `json:"id"`
Avatar string `json:"avatar"`
} `json:"users,omitempty"`
AuditLogEntries []struct {
TargetID string `json:"target_id"`
Changes []struct {
NewValue interface{} `json:"new_value"`
OldValue interface{} `json:"old_value"`
Key string `json:"key"`
} `json:"changes,omitempty"`
UserID string `json:"user_id"`
ID string `json:"id"`
ActionType int `json:"action_type"`
Options struct {
DeleteMembersDay string `json:"delete_member_days"`
MembersRemoved string `json:"members_removed"`
ChannelID string `json:"channel_id"`
Count string `json:"count"`
ID string `json:"id"`
Type string `json:"type"`
RoleName string `json:"role_name"`
} `json:"options,omitempty"`
Reason string `json:"reason"`
} `json:"audit_log_entries"`
}
// Block contains Discord Audit Log Action Types
const (
AuditLogActionGuildUpdate = 1
AuditLogActionChannelCreate = 10
AuditLogActionChannelUpdate = 11
AuditLogActionChannelDelete = 12
AuditLogActionChannelOverwriteCreate = 13
AuditLogActionChannelOverwriteUpdate = 14
AuditLogActionChannelOverwriteDelete = 15
AuditLogActionMemberKick = 20
AuditLogActionMemberPrune = 21
AuditLogActionMemberBanAdd = 22
AuditLogActionMemberBanRemove = 23
AuditLogActionMemberUpdate = 24
AuditLogActionMemberRoleUpdate = 25
AuditLogActionRoleCreate = 30
AuditLogActionRoleUpdate = 31
AuditLogActionRoleDelete = 32
AuditLogActionInviteCreate = 40
AuditLogActionInviteUpdate = 41
AuditLogActionInviteDelete = 42
AuditLogActionWebhookCreate = 50
AuditLogActionWebhookUpdate = 51
AuditLogActionWebhookDelete = 52
AuditLogActionEmojiCreate = 60
AuditLogActionEmojiUpdate = 61
AuditLogActionEmojiDelete = 62
AuditLogActionMessageDelete = 72
)
// A UserGuildSettingsChannelOverride stores data for a channel override for a users guild settings. // A UserGuildSettingsChannelOverride stores data for a channel override for a users guild settings.
type UserGuildSettingsChannelOverride struct { type UserGuildSettingsChannelOverride struct {
Muted bool `json:"muted"` Muted bool `json:"muted"`
@ -553,6 +842,7 @@ type MessageReaction struct {
MessageID string `json:"message_id"` MessageID string `json:"message_id"`
Emoji Emoji `json:"emoji"` Emoji Emoji `json:"emoji"`
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
GuildID string `json:"guild_id,omitempty"`
} }
// GatewayBotResponse stores the data for the gateway/bot response // GatewayBotResponse stores the data for the gateway/bot response
@ -629,7 +919,9 @@ const (
PermissionKickMembers | PermissionKickMembers |
PermissionBanMembers | PermissionBanMembers |
PermissionManageServer | PermissionManageServer |
PermissionAdministrator PermissionAdministrator |
PermissionManageWebhooks |
PermissionManageEmojis
) )
// Block contains Discord JSON Error Response codes // Block contains Discord JSON Error Response codes
@ -648,6 +940,7 @@ const (
ErrCodeUnknownToken = 10012 ErrCodeUnknownToken = 10012
ErrCodeUnknownUser = 10013 ErrCodeUnknownUser = 10013
ErrCodeUnknownEmoji = 10014 ErrCodeUnknownEmoji = 10014
ErrCodeUnknownWebhook = 10015
ErrCodeBotsCannotUseEndpoint = 20001 ErrCodeBotsCannotUseEndpoint = 20001
ErrCodeOnlyBotsCanUseEndpoint = 20002 ErrCodeOnlyBotsCanUseEndpoint = 20002

View file

@ -11,7 +11,6 @@ package discordgo
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"time" "time"
) )
@ -54,5 +53,5 @@ func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTErro
} }
func (r RESTError) Error() string { func (r RESTError) Error() string {
return fmt.Sprintf("HTTP %s, %s", r.Response.Status, r.ResponseBody) return "HTTP " + r.Response.Status + ", " + string(r.ResponseBody)
} }

52
user.go
View file

@ -1,31 +1,51 @@
package discordgo package discordgo
import ( import "strings"
"fmt"
"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"` // The ID of the user.
Email string `json:"email"` ID string `json:"id"`
Username string `json:"username"`
Avatar string `json:"avatar"` // The email of the user. This is only present when
// the application possesses the email scope for the user.
Email string `json:"email"`
// The user's username.
Username string `json:"username"`
// The hash of the user's avatar. Use Session.UserAvatar
// to retrieve the avatar itself.
Avatar string `json:"avatar"`
// The user's chosen language option.
Locale string `json:"locale"`
// The discriminator of the user (4 numbers after name).
Discriminator string `json:"discriminator"` Discriminator string `json:"discriminator"`
Token string `json:"token"`
Verified bool `json:"verified"` // The token of the user. This is only present for
MFAEnabled bool `json:"mfa_enabled"` // the user represented by the current session.
Bot bool `json:"bot"` Token string `json:"token"`
// Whether the user's email is verified.
Verified bool `json:"verified"`
// Whether the user has multi-factor authentication enabled.
MFAEnabled bool `json:"mfa_enabled"`
// Whether the user is a bot.
Bot bool `json:"bot"`
} }
// String returns a unique identifier of the form username#discriminator // String returns a unique identifier of the form username#discriminator
func (u *User) String() string { func (u *User) String() string {
return fmt.Sprintf("%s#%s", u.Username, u.Discriminator) return u.Username + "#" + u.Discriminator
} }
// Mention return a string which mentions the user // Mention return a string which mentions the user
func (u *User) Mention() string { func (u *User) Mention() string {
return fmt.Sprintf("<@%s>", u.ID) return "<@" + u.ID + ">"
} }
// AvatarURL returns a URL to the user's avatar. // AvatarURL returns a URL to the user's avatar.
@ -34,7 +54,9 @@ func (u *User) Mention() string {
// be added to the URL. // be added to the URL.
func (u *User) AvatarURL(size string) string { func (u *User) AvatarURL(size string) string {
var URL string var URL string
if strings.HasPrefix(u.Avatar, "a_") { if u.Avatar == "" {
URL = EndpointDefaultUserAvatar(u.Discriminator)
} else if strings.HasPrefix(u.Avatar, "a_") {
URL = EndpointUserAvatarAnimated(u.ID, u.Avatar) URL = EndpointUserAvatarAnimated(u.ID, u.Avatar)
} else { } else {
URL = EndpointUserAvatar(u.ID, u.Avatar) URL = EndpointUserAvatar(u.ID, u.Avatar)

View file

@ -14,6 +14,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -103,7 +104,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) {
defer v.Unlock() defer v.Unlock()
if err != nil { if err != nil {
v.speaking = false v.speaking = false
v.log(LogError, "Speaking() write json error:", err) v.log(LogError, "Speaking() write json error, %s", err)
return return
} }
@ -135,7 +136,6 @@ func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err
// Disconnect disconnects from this voice channel and closes the websocket // Disconnect disconnects from this voice channel and closes the websocket
// and udp connections to Discord. // and udp connections to Discord.
// !!! NOTE !!! this function may be removed in favour of ChannelVoiceLeave
func (v *VoiceConnection) Disconnect() (err error) { func (v *VoiceConnection) Disconnect() (err error) {
// Send a OP4 with a nil channel to disconnect // Send a OP4 with a nil channel to disconnect
@ -180,7 +180,7 @@ func (v *VoiceConnection) Close() {
v.log(LogInformational, "closing udp") v.log(LogInformational, "closing udp")
err := v.udpConn.Close() err := v.udpConn.Close()
if err != nil { if err != nil {
v.log(LogError, "error closing udp connection: ", err) v.log(LogError, "error closing udp connection, %s", err)
} }
v.udpConn = nil v.udpConn = nil
} }
@ -299,7 +299,7 @@ func (v *VoiceConnection) open() (err error) {
} }
// Connect to VoiceConnection Websocket // Connect to VoiceConnection Websocket
vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80")) vg := "wss://" + strings.TrimSuffix(v.endpoint, ":80")
v.log(LogInformational, "connecting to voice endpoint %s", vg) v.log(LogInformational, "connecting to voice endpoint %s", vg)
v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil) v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil)
if err != nil { if err != nil {
@ -542,7 +542,7 @@ func (v *VoiceConnection) udpOpen() (err error) {
return fmt.Errorf("empty endpoint") return fmt.Errorf("empty endpoint")
} }
host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.op2.Port) host := strings.TrimSuffix(v.endpoint, ":80") + ":" + strconv.Itoa(v.op2.Port)
addr, err := net.ResolveUDPAddr("udp", host) addr, err := net.ResolveUDPAddr("udp", host)
if err != nil { if err != nil {
v.log(LogWarning, "error resolving udp host %s, %s", host, err) v.log(LogWarning, "error resolving udp host %s, %s", host, err)

View file

@ -86,6 +86,10 @@ func (s *Session) Open() error {
return err return err
} }
s.wsConn.SetCloseHandler(func(code int, text string) error {
return nil
})
defer func() { defer func() {
// because of this, all code below must set err to the error // because of this, all code below must set err to the error
// when exiting with an error :) Maybe someone has a better // when exiting with an error :) Maybe someone has a better
@ -263,6 +267,13 @@ type helloOp struct {
// FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart. // FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart.
const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond
// HeartbeatLatency returns the latency between heartbeat acknowledgement and heartbeat send.
func (s *Session) HeartbeatLatency() time.Duration {
return s.LastHeartbeatAck.Sub(s.LastHeartbeatSent)
}
// 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.
@ -283,8 +294,9 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
last := s.LastHeartbeatAck last := s.LastHeartbeatAck
s.RUnlock() s.RUnlock()
sequence := atomic.LoadInt64(s.sequence) sequence := atomic.LoadInt64(s.sequence)
s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) s.log(LogDebug, "sending gateway websocket heartbeat seq %d", sequence)
s.wsMutex.Lock() s.wsMutex.Lock()
s.LastHeartbeatSent = time.Now().UTC()
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 || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) {
@ -323,16 +335,8 @@ type updateStatusOp struct {
Data UpdateStatusData `json:"d"` Data UpdateStatusData `json:"d"`
} }
// UpdateStreamingStatus is used to update the user's streaming status. func newUpdateStatusData(idle int, gameType GameType, game, url string) *UpdateStatusData {
// If idle>0 then set status to idle. usd := &UpdateStatusData{
// If game!="" then set game.
// If game!="" and url!="" then set the status type to streaming with the URL set.
// if otherwise, set status to active, and no game.
func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) {
s.log(LogInformational, "called")
usd := UpdateStatusData{
Status: "online", Status: "online",
} }
@ -341,10 +345,6 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
} }
if game != "" { if game != "" {
gameType := GameTypeGame
if url != "" {
gameType = GameTypeStreaming
}
usd.Game = &Game{ usd.Game = &Game{
Name: game, Name: game,
Type: gameType, Type: gameType,
@ -352,7 +352,35 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err
} }
} }
return s.UpdateStatusComplex(usd) return usd
}
// UpdateStatus is used to update the user's status.
// If idle>0 then set status to idle.
// If game!="" then set game.
// if otherwise, set status to active, and no game.
func (s *Session) UpdateStatus(idle int, game string) (err error) {
return s.UpdateStatusComplex(*newUpdateStatusData(idle, GameTypeGame, game, ""))
}
// UpdateStreamingStatus is used to update the user's streaming status.
// If idle>0 then set status to idle.
// If game!="" then set game.
// If game!="" and url!="" then set the status type to streaming with the URL set.
// if otherwise, set status to active, and no game.
func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) {
gameType := GameTypeGame
if url != "" {
gameType = GameTypeStreaming
}
return s.UpdateStatusComplex(*newUpdateStatusData(idle, gameType, game, url))
}
// UpdateListeningStatus is used to set the user to "Listening to..."
// If game!="" then set to what user is listening to
// Else, set user to active and no game.
func (s *Session) UpdateListeningStatus(game string) (err error) {
return s.UpdateStatusComplex(*newUpdateStatusData(0, GameTypeListening, game, ""))
} }
// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo. // UpdateStatusComplex allows for sending the raw status update data untouched by discordgo.
@ -371,14 +399,6 @@ func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) {
return return
} }
// UpdateStatus is used to update the user's status.
// If idle>0 then set status to idle.
// If game!="" then set game.
// if otherwise, set status to active, and no game.
func (s *Session) UpdateStatus(idle int, game string) (err error) {
return s.UpdateStreamingStatus(idle, game, "")
}
type requestGuildMembersData struct { type requestGuildMembersData struct {
GuildID string `json:"guild_id"` GuildID string `json:"guild_id"`
Query string `json:"query"` Query string `json:"query"`
@ -508,7 +528,7 @@ func (s *Session) onEvent(messageType int, message []byte) (*Event, error) {
s.Lock() s.Lock()
s.LastHeartbeatAck = time.Now().UTC() s.LastHeartbeatAck = time.Now().UTC()
s.Unlock() s.Unlock()
s.log(LogInformational, "got heartbeat ACK") s.log(LogDebug, "got heartbeat ACK")
return e, nil return e, nil
} }
@ -615,6 +635,30 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi
return return
} }
// ChannelVoiceJoinManual initiates a voice session to a voice channel, but does not complete it.
//
// This should only be used when the VoiceServerUpdate will be intercepted and used elsewhere.
//
// gID : Guild ID of the channel to join.
// cID : Channel ID of the channel to join.
// mute : If true, you will be set to muted upon joining.
// deaf : If true, you will be set to deafened upon joining.
func (s *Session) ChannelVoiceJoinManual(gID, cID string, mute, deaf bool) (err error) {
s.log(LogInformational, "called")
// Send the request to Discord that we want to join the voice channel
data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}}
s.wsMutex.Lock()
err = s.wsConn.WriteJSON(data)
s.wsMutex.Unlock()
if err != nil {
return
}
return
}
// onVoiceStateUpdate handles Voice State Update events on the data websocket. // onVoiceStateUpdate handles Voice State Update events on the data websocket.
func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) { func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) {
@ -732,11 +776,8 @@ func (s *Session) identify() error {
s.wsMutex.Lock() s.wsMutex.Lock()
err := s.wsConn.WriteJSON(op) err := s.wsConn.WriteJSON(op)
s.wsMutex.Unlock() s.wsMutex.Unlock()
if err != nil {
return err
}
return nil return err
} }
func (s *Session) reconnect() { func (s *Session) reconnect() {