Browse Source

Merge branch 'develop'

Bruce Marriner 5 years ago
parent
commit
8baec95e44
14 changed files with 229 additions and 28 deletions
  1. 1 1
      README.md
  2. 2 1
      discord.go
  3. 1 1
      docs/index.md
  4. 8 4
      endpoints.go
  5. 5 3
      events.go
  6. 2 0
      go.mod
  7. 79 0
      message.go
  8. 19 0
      oauth2.go
  9. 15 3
      restapi.go
  10. 7 0
      state.go
  11. 60 1
      structs.go
  12. 17 0
      util.go
  13. 3 2
      voice.go
  14. 10 12
      wsapi.go

File diff suppressed because it is too large
+ 1 - 1
README.md


+ 2 - 1
discord.go

@@ -21,7 +21,7 @@ import (
 )
 
 // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
-const VERSION = "0.19.0"
+const VERSION = "0.20.0-alpha"
 
 // ErrMFA will be risen by New when the user has 2FA.
 var ErrMFA = errors.New("account has 2FA enabled")
@@ -58,6 +58,7 @@ func New(args ...interface{}) (s *Session, err error) {
 		ShardCount:             1,
 		MaxRestRetries:         3,
 		Client:                 &http.Client{Timeout: (20 * time.Second)},
+		UserAgent:              "DiscordBot (https://github.com/bwmarrin/discordgo, v" + VERSION + ")",
 		sequence:               new(int64),
 		LastHeartbeatAck:       time.Now().UTC(),
 	}

+ 1 - 1
docs/index.md

@@ -30,4 +30,4 @@ information and support for DiscordGo.  There's also a chance to make some
 friends :)
 
 * Join the [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) chat server dedicated to Go programming.
-* Join the [Discord API](https://discord.gg/0SBTUU1wZTWT6sqd) chat server dedicated to the Discord API.
+* Join the [Discord API](https://discordapp.com/invite/discord-API) chat server dedicated to the Discord API.

+ 8 - 4
endpoints.go

@@ -38,6 +38,7 @@ var (
 	EndpointCDNIcons        = EndpointCDN + "icons/"
 	EndpointCDNSplashes     = EndpointCDN + "splashes/"
 	EndpointCDNChannelIcons = EndpointCDN + "channel-icons/"
+	EndpointCDNBanners      = EndpointCDN + "banners/"
 
 	EndpointAuth           = EndpointAPI + "auth/"
 	EndpointLogin          = EndpointAuth + "login"
@@ -92,11 +93,13 @@ var (
 	EndpointGuildEmbed           = func(gID string) string { return EndpointGuilds + gID + "/embed" }
 	EndpointGuildPrune           = func(gID string) string { return EndpointGuilds + gID + "/prune" }
 	EndpointGuildIcon            = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" }
+	EndpointGuildIconAnimated    = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".gif" }
 	EndpointGuildSplash          = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" }
 	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 }
+	EndpointGuildBanner          = func(gID, hash string) string { return EndpointCDNBanners + gID + "/" + hash + ".png" }
 
 	EndpointChannel                   = func(cID string) string { return EndpointChannels + cID }
 	EndpointChannelPermissions        = func(cID string) string { return EndpointChannels + cID + "/permissions" }
@@ -139,8 +142,9 @@ var (
 	EndpointEmoji         = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" }
 	EndpointEmojiAnimated = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".gif" }
 
-	EndpointOauth2          = EndpointAPI + "oauth2/"
-	EndpointApplications    = EndpointOauth2 + "applications"
-	EndpointApplication     = func(aID string) string { return EndpointApplications + "/" + aID }
-	EndpointApplicationsBot = func(aID string) string { return EndpointApplications + "/" + aID + "/bot" }
+	EndpointOauth2            = EndpointAPI + "oauth2/"
+	EndpointApplications      = EndpointOauth2 + "applications"
+	EndpointApplication       = func(aID string) string { return EndpointApplications + "/" + aID }
+	EndpointApplicationsBot   = func(aID string) string { return EndpointApplications + "/" + aID + "/bot" }
+	EndpointApplicationAssets = func(aID string) string { return EndpointApplications + "/" + aID + "/assets" }
 )

+ 5 - 3
events.go

@@ -10,15 +10,15 @@ import (
 //go:generate go run tools/cmd/eventhandlers/main.go
 
 // Connect is the data for a Connect event.
-// This is a sythetic event and is not dispatched by Discord.
+// This is a synthetic event and is not dispatched by Discord.
 type Connect struct{}
 
 // Disconnect is the data for a Disconnect event.
-// This is a sythetic event and is not dispatched by Discord.
+// This is a synthetic event and is not dispatched by Discord.
 type Disconnect struct{}
 
 // RateLimit is the data for a RateLimit event.
-// This is a sythetic event and is not dispatched by Discord.
+// This is a synthetic event and is not dispatched by Discord.
 type RateLimit struct {
 	*TooManyRequests
 	URL string
@@ -162,6 +162,8 @@ type MessageCreate struct {
 // MessageUpdate is the data for a MessageUpdate event.
 type MessageUpdate struct {
 	*Message
+	// BeforeUpdate will be nil if the Message was not previously cached in the state cache.
+	BeforeUpdate *Message `json:"-"`
 }
 
 // MessageDelete is the data for a MessageDelete event.

+ 2 - 0
go.mod

@@ -4,3 +4,5 @@ require (
 	github.com/gorilla/websocket v1.4.0
 	golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16
 )
+
+go 1.13

+ 79 - 0
message.go

@@ -28,6 +28,11 @@ const (
 	MessageTypeChannelIconChange
 	MessageTypeChannelPinnedMessage
 	MessageTypeGuildMemberJoin
+	MessageTypeUserPremiumGuildSubscription
+	MessageTypeUserPremiumGuildSubscriptionTierOne
+	MessageTypeUserPremiumGuildSubscriptionTierTwo
+	MessageTypeUserPremiumGuildSubscriptionTierThree
+	MessageTypeChannelFollowAdd
 )
 
 // A Message stores all data related to a specific Discord message.
@@ -80,11 +85,39 @@ type Message struct {
 	// A list of reactions to the message.
 	Reactions []*MessageReactions `json:"reactions"`
 
+	// Whether the message is pinned or not.
+	Pinned bool `json:"pinned"`
+
 	// 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"`
+
+	// Member properties for this message's author,
+	// contains only partial information
+	Member *Member `json:"member"`
+
+	// Channels specifically mentioned in this message
+	// Not all channel mentions in a message will appear in mention_channels.
+	// Only textual channels that are visible to everyone in a lurkable guild will ever be included.
+	// Only crossposted messages (via Channel Following) currently include mention_channels at all.
+	// If no mentions in the message meet these requirements, this field will not be sent.
+	MentionChannels []*Channel `json:"mention_channels"`
+
+	// Is sent with Rich Presence-related chat embeds
+	Activity *MessageActivity `json:"activity"`
+
+	// Is sent with Rich Presence-related chat embeds
+	Application *MessageApplication `json:"application"`
+
+	// MessageReference contains reference data sent with crossposted messages
+	MessageReference *MessageReference `json:"message_reference"`
+
+	// The flags of the message, which describe extra features of a message.
+	// 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 flag.
+	Flags int `json:"flags"`
 }
 
 // File stores info about files you e.g. send in messages.
@@ -225,6 +258,52 @@ type MessageReactions struct {
 	Emoji *Emoji `json:"emoji"`
 }
 
+// MessageActivity is sent with Rich Presence-related chat embeds
+type MessageActivity struct {
+	Type    MessageActivityType `json:"type"`
+	PartyID string              `json:"party_id"`
+}
+
+// MessageActivityType is the type of message activity
+type MessageActivityType int
+
+// Constants for the different types of Message Activity
+const (
+	MessageActivityTypeJoin = iota + 1
+	MessageActivityTypeSpectate
+	MessageActivityTypeListen
+	MessageActivityTypeJoinRequest
+)
+
+// MessageFlag describes an extra feature of the message
+type MessageFlag int
+
+// Constants for the different bit offsets of Message Flags
+const (
+	// This message has been published to subscribed channels (via Channel Following)
+	MessageFlagCrossposted = 1 << iota
+	// This message originated from a message in another channel (via Channel Following)
+	MessageFlagIsCrosspost
+	// Do not include any embeds when serializing this message
+	MessageFlagSuppressEmbeds
+)
+
+// MessageApplication is sent with Rich Presence-related chat embeds
+type MessageApplication struct {
+	ID          string `json:"id"`
+	CoverImage  string `json:"cover_image"`
+	Description string `json:"description"`
+	Icon        string `json:"icon"`
+	Name        string `json:"name"`
+}
+
+// MessageReference contains reference data sent with crossposted messages
+type MessageReference struct {
+	MessageID string `json:"message_id"`
+	ChannelID string `json:"channel_id"`
+	GuildID   string `json:"guild_id"`
+}
+
 // ContentWithMentionsReplaced will replace all @<id> mentions with the
 // username of the mention.
 func (m *Message) ContentWithMentionsReplaced() (content string) {

+ 19 - 0
oauth2.go

@@ -105,6 +105,25 @@ func (s *Session) ApplicationDelete(appID string) (err error) {
 	return
 }
 
+// Asset struct stores values for an asset of an application
+type Asset struct {
+	Type int    `json:"type"`
+	ID   string `json:"id"`
+	Name string `json:"name"`
+}
+
+// ApplicationAssets returns an application's assets
+func (s *Session) ApplicationAssets(appID string) (ass []*Asset, err error) {
+
+	body, err := s.RequestWithBucketID("GET", EndpointApplicationAssets(appID), nil, EndpointApplicationAssets(""))
+	if err != nil {
+		return
+	}
+
+	err = unmarshal(body, &ass)
+	return
+}
+
 // ------------------------------------------------------------------------------------------------
 // Code specific to Discord OAuth2 Application Bots
 // ------------------------------------------------------------------------------------------------

+ 15 - 3
restapi.go

@@ -90,7 +90,7 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b
 
 	req.Header.Set("Content-Type", contentType)
 	// TODO: Make a configurable static variable.
-	req.Header.Set("User-Agent", "DiscordBot (https://github.com/bwmarrin/discordgo, v"+VERSION+")")
+	req.Header.Set("User-Agent", s.UserAgent)
 
 	if s.Debug {
 		for k, v := range req.Header {
@@ -2067,14 +2067,20 @@ func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook,
 // WebhookExecute executes a webhook.
 // webhookID: The ID of a webhook.
 // token    : The auth token for the webhook
-func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (err error) {
+// wait     : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise)
+func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (st *Message, err error) {
 	uri := EndpointWebhookToken(webhookID, token)
 
 	if wait {
 		uri += "?wait=true"
 	}
 
-	_, err = s.RequestWithBucketID("POST", uri, data, EndpointWebhookToken("", ""))
+	response, err := s.RequestWithBucketID("POST", uri, data, EndpointWebhookToken("", ""))
+	if !wait || err != nil {
+		return
+	}
+
+	err = unmarshal(response, &st)
 
 	return
 }
@@ -2085,6 +2091,8 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho
 // emojiID   : Either the unicode emoji for the reaction, or a guild emoji identifier.
 func (s *Session) MessageReactionAdd(channelID, messageID, emojiID string) error {
 
+	// emoji such as  #⃣ need to have # escaped
+	emojiID = strings.Replace(emojiID, "#", "%23", -1)
 	_, err := s.RequestWithBucketID("PUT", EndpointMessageReaction(channelID, messageID, emojiID, "@me"), nil, EndpointMessageReaction(channelID, "", "", ""))
 
 	return err
@@ -2097,6 +2105,8 @@ func (s *Session) MessageReactionAdd(channelID, messageID, emojiID string) error
 // userID	 : @me or ID of the user to delete the reaction for.
 func (s *Session) MessageReactionRemove(channelID, messageID, emojiID, userID string) error {
 
+	// emoji such as  #⃣ need to have # escaped
+	emojiID = strings.Replace(emojiID, "#", "%23", -1)
 	_, err := s.RequestWithBucketID("DELETE", EndpointMessageReaction(channelID, messageID, emojiID, userID), nil, EndpointMessageReaction(channelID, "", "", ""))
 
 	return err
@@ -2118,6 +2128,8 @@ func (s *Session) MessageReactionsRemoveAll(channelID, messageID string) error {
 // emojiID   : Either the unicode emoji for the reaction, or a guild emoji identifier.
 // limit    : max number of users to return (max 100)
 func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit int) (st []*User, err error) {
+	// emoji such as  #⃣ need to have # escaped
+	emojiID = strings.Replace(emojiID, "#", "%23", -1)
 	uri := EndpointMessageReactions(channelID, messageID, emojiID)
 
 	v := url.Values{}

+ 7 - 0
state.go

@@ -882,6 +882,13 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) {
 		}
 	case *MessageUpdate:
 		if s.MaxMessageCount != 0 {
+			var old *Message
+			old, err = s.Message(t.ChannelID, t.ID)
+			if err == nil {
+				oldCopy := *old
+				t.BeforeUpdate = &oldCopy
+			}
+
 			err = s.MessageAdd(t.Message)
 		}
 	case *MessageDelete:

+ 60 - 1
structs.go

@@ -15,6 +15,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"strings"
 	"sync"
 	"time"
 
@@ -82,6 +83,9 @@ type Session struct {
 	// The http client used for REST requests
 	Client *http.Client
 
+	// The user agent used for REST APIs
+	UserAgent string
+
 	// Stores the last HeartbeatAck that was recieved (in UTC)
 	LastHeartbeatAck time.Time
 
@@ -196,6 +200,8 @@ const (
 	ChannelTypeGuildVoice
 	ChannelTypeGroupDM
 	ChannelTypeGuildCategory
+	ChannelTypeGuildNews
+	ChannelTypeGuildStore
 )
 
 // A Channel holds all data related to an individual Discord channel.
@@ -220,6 +226,10 @@ type Channel struct {
 	// guaranteed to be an ID of a valid message.
 	LastMessageID string `json:"last_message_id"`
 
+	// The timestamp of the last pinned message in the channel.
+	// Empty if the channel has no pinned messages.
+	LastPinTimestamp Timestamp `json:"last_pin_timestamp"`
+
 	// Whether the channel is marked as NSFW.
 	NSFW bool `json:"nsfw"`
 
@@ -247,6 +257,10 @@ type Channel struct {
 
 	// The ID of the parent channel, if the channel is under a category
 	ParentID string `json:"parent_id"`
+
+	// Amount of seconds a user has to wait before sending another message (0-21600)
+	// bots, as well as users with the permission manage_messages or manage_channel, are unaffected
+	RateLimitPerUser int `json:"rate_limit_per_user"`
 }
 
 // Mention returns a string which mentions the channel
@@ -283,6 +297,7 @@ type Emoji struct {
 	Managed       bool     `json:"managed"`
 	RequireColons bool     `json:"require_colons"`
 	Animated      bool     `json:"animated"`
+	Available     bool     `json:"available"`
 }
 
 // MessageFormat returns a correctly formatted Emoji for use in Message content and embeds
@@ -339,6 +354,17 @@ const (
 	MfaLevelElevated
 )
 
+// PremiumTier type definition
+type PremiumTier int
+
+// Constants for PremiumTier levels from 0 to 3 inclusive
+const (
+	PremiumTierNone PremiumTier = iota
+	PremiumTier1
+	PremiumTier2
+	PremiumTier3
+)
+
 // A Guild holds all data related to a specific Discord Guild.  Guilds are also
 // sometimes referred to as Servers in the Discord client.
 type Guild struct {
@@ -443,6 +469,34 @@ type Guild struct {
 
 	// The Channel ID to which system messages are sent (eg join and leave messages)
 	SystemChannelID string `json:"system_channel_id"`
+
+	// the vanity url code for the guild
+	VanityURLCode string `json:"vanity_url_code"`
+
+	// the description for the guild
+	Description string `json:"description"`
+
+	// The hash of the guild's banner
+	Banner string `json:"banner"`
+
+	// The premium tier of the guild
+	PremiumTier PremiumTier `json:"premium_tier"`
+
+	// The total number of users currently boosting this server
+	PremiumSubscriptionCount int `json:"premium_subscription_count"`
+}
+
+// IconURL returns a URL to the guild's icon.
+func (g *Guild) IconURL() string {
+	if g.Icon == "" {
+		return ""
+	}
+
+	if strings.HasPrefix(g.Icon, "a_") {
+		return EndpointGuildIconAnimated(g.ID, g.Icon)
+	}
+
+	return EndpointGuildIcon(g.ID, g.Icon)
 }
 
 // A UserGuild holds a brief version of a Guild
@@ -617,6 +671,9 @@ type Member struct {
 
 	// A list of IDs of the roles which are possessed by the member.
 	Roles []string `json:"roles"`
+
+	// When the user used their Nitro boost on the server
+	PremiumSince Timestamp `json:"premium_since"`
 }
 
 // Mention creates a member mention
@@ -872,6 +929,7 @@ const (
 	PermissionVoiceDeafenMembers
 	PermissionVoiceMoveMembers
 	PermissionVoiceUseVAD
+	PermissionVoicePrioritySpeaker = 1 << (iota + 2)
 )
 
 // Constants for general management.
@@ -907,7 +965,8 @@ const (
 		PermissionVoiceMuteMembers |
 		PermissionVoiceDeafenMembers |
 		PermissionVoiceMoveMembers |
-		PermissionVoiceUseVAD
+		PermissionVoiceUseVAD |
+		PermissionVoicePrioritySpeaker
 	PermissionAllChannel = PermissionAllText |
 		PermissionAllVoice |
 		PermissionCreateInstantInvite |

+ 17 - 0
util.go

@@ -0,0 +1,17 @@
+package discordgo
+
+import (
+	"strconv"
+	"time"
+)
+
+// SnowflakeTimestamp returns the creation time of a Snowflake ID relative to the creation of Discord.
+func SnowflakeTimestamp(ID string) (t time.Time, err error) {
+	i, err := strconv.ParseInt(ID, 10, 64)
+	if err != nil {
+		return
+	}
+	timestamp := (i >> 22) + 1420070400000
+	t = time.Unix(timestamp/1000, 0)
+	return
+}

+ 3 - 2
voice.go

@@ -243,6 +243,7 @@ type voiceOP2 struct {
 	Port              int           `json:"port"`
 	Modes             []string      `json:"modes"`
 	HeartbeatInterval time.Duration `json:"heartbeat_interval"`
+	IP                string        `json:"ip"`
 }
 
 // WaitUntilConnected waits for the Voice Connection to
@@ -542,7 +543,7 @@ func (v *VoiceConnection) udpOpen() (err error) {
 		return fmt.Errorf("empty endpoint")
 	}
 
-	host := strings.TrimSuffix(v.endpoint, ":80") + ":" + strconv.Itoa(v.op2.Port)
+	host := v.op2.IP + ":" + strconv.Itoa(v.op2.Port)
 	addr, err := net.ResolveUDPAddr("udp", host)
 	if err != nil {
 		v.log(LogWarning, "error resolving udp host %s, %s", host, err)
@@ -593,7 +594,7 @@ func (v *VoiceConnection) udpOpen() (err error) {
 	}
 
 	// Grab port from position 68 and 69
-	port := binary.LittleEndian.Uint16(rb[68:70])
+	port := binary.BigEndian.Uint16(rb[68:70])
 
 	// Take the data from above and send it back to Discord to finalize
 	// the UDP connection handshake.

+ 10 - 12
wsapi.go

@@ -261,7 +261,6 @@ type heartbeatOp struct {
 
 type helloOp struct {
 	HeartbeatInterval time.Duration `json:"heartbeat_interval"`
-	Trace             []string      `json:"_trace"`
 }
 
 // FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart.
@@ -615,11 +614,7 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi
 	voice.session = s
 	voice.Unlock()
 
-	// 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()
+	err = s.ChannelVoiceJoinManual(gID, cID, mute, deaf)
 	if err != nil {
 		return
 	}
@@ -640,22 +635,25 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi
 // 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.
+//    cID     : Channel ID of the channel to join, leave empty to disconnect.
 //    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")
 
+	var channelID *string
+	if cID == "" {
+		channelID = nil
+	} else {
+		channelID = &cID
+	}
+
 	// Send the request to Discord that we want to join the voice channel
-	data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}}
+	data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, channelID, mute, deaf}}
 	s.wsMutex.Lock()
 	err = s.wsConn.WriteJSON(data)
 	s.wsMutex.Unlock()
-	if err != nil {
-		return
-	}
-
 	return
 }