From 8164119cac63d0c0a0134ddb2c5eb974a692bceb Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 27 Sep 2016 21:44:16 -0700 Subject: [PATCH 1/4] Support a very light state in all cases to support b1nzy's upcoming PR --- discord.go | 1 + state.go | 85 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/discord.go b/discord.go index 776f143..77bde70 100644 --- a/discord.go +++ b/discord.go @@ -237,6 +237,7 @@ func (s *Session) initialize() { s.AddHandler(s.onResumed) s.AddHandler(s.onVoiceServerUpdate) s.AddHandler(s.onVoiceStateUpdate) + s.AddHandler(s.State.onReady) s.AddHandler(s.State.onInterface) } diff --git a/state.go b/state.go index d2d90f3..798c18c 100644 --- a/state.go +++ b/state.go @@ -55,33 +55,6 @@ func NewState() *State { } } -// OnReady takes a Ready event and updates all internal state. -func (s *State) OnReady(r *Ready) error { - if s == nil { - return ErrNilState - } - - s.Lock() - defer s.Unlock() - - s.Ready = *r - - for _, g := range s.Guilds { - s.guildMap[g.ID] = g - - for _, c := range g.Channels { - c.GuildID = g.ID - s.channelMap[c.ID] = c - } - } - - for _, c := range s.PrivateChannels { - s.channelMap[c.ID] = c - } - - return nil -} - // GuildAdd adds a guild to the current world state, or // updates it if it already exists. func (s *State) GuildAdd(guild *Guild) error { @@ -619,6 +592,59 @@ func (s *State) Message(channelID, messageID string) (*Message, error) { return nil, errors.New("Message not found.") } +// OnReady takes a Ready event and updates all internal state. +func (s *State) onReady(se *Session, r *Ready) (err error) { + // We must track at least the current user for Voice, if state is + // either nil or disabled, we will set a very simple state item. + if s == nil || !se.StateEnabled { + ready := Ready{ + Version: r.Version, + SessionID: r.SessionID, + HeartbeatInterval: r.HeartbeatInterval, + User: r.User, + } + + if s == nil { + *s = State{ + TrackChannels: false, + TrackEmojis: false, + TrackMembers: false, + TrackRoles: false, + TrackVoice: false, + guildMap: make(map[string]*Guild), + channelMap: make(map[string]*Channel), + } + } + + s.Lock() + defer s.Unlock() + + s.Ready = ready + + return nil + } + + s.Lock() + defer s.Unlock() + + s.Ready = *r + + for _, g := range s.Guilds { + s.guildMap[g.ID] = g + + for _, c := range g.Channels { + c.GuildID = g.ID + s.channelMap[c.ID] = c + } + } + + for _, c := range s.PrivateChannels { + s.channelMap[c.ID] = c + } + + return nil +} + // onInterface handles all events related to states. func (s *State) onInterface(se *Session, i interface{}) (err error) { if s == nil { @@ -629,8 +655,6 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { } switch t := i.(type) { - case *Ready: - err = s.OnReady(t) case *GuildCreate: err = s.GuildAdd(t.Guild) case *GuildUpdate: @@ -702,6 +726,9 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { // userID : The ID of the user to calculate permissions for. // channelID : The ID of the channel to calculate permission for. func (s *State) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { + if s == nil { + return 0, ErrNilState + } channel, err := s.Channel(channelID) if err != nil { From 6c53613186964a2c3fdecafea9bf6102b7b76d5c Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 27 Sep 2016 22:02:28 -0700 Subject: [PATCH 2/4] Thanks govet. --- state.go | 31 ++++++++++--------------------- wsapi.go | 11 +++++++++++ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/state.go b/state.go index 798c18c..aa7997e 100644 --- a/state.go +++ b/state.go @@ -594,9 +594,16 @@ func (s *State) Message(channelID, messageID string) (*Message, error) { // OnReady takes a Ready event and updates all internal state. func (s *State) onReady(se *Session, r *Ready) (err error) { - // We must track at least the current user for Voice, if state is - // either nil or disabled, we will set a very simple state item. - if s == nil || !se.StateEnabled { + if s == nil { + return ErrNilState + } + + s.Lock() + defer s.Unlock() + + // We must track at least the current user for Voice, even + // if state is disabled, store the bare essentials. + if !se.StateEnabled { ready := Ready{ Version: r.Version, SessionID: r.SessionID, @@ -604,29 +611,11 @@ func (s *State) onReady(se *Session, r *Ready) (err error) { User: r.User, } - if s == nil { - *s = State{ - TrackChannels: false, - TrackEmojis: false, - TrackMembers: false, - TrackRoles: false, - TrackVoice: false, - guildMap: make(map[string]*Guild), - channelMap: make(map[string]*Channel), - } - } - - s.Lock() - defer s.Unlock() - s.Ready = ready return nil } - s.Lock() - defer s.Unlock() - s.Ready = *r for _, g := range s.Guilds { diff --git a/wsapi.go b/wsapi.go index a19c384..4776fdd 100644 --- a/wsapi.go +++ b/wsapi.go @@ -47,6 +47,17 @@ func (s *Session) Open() (err error) { } }() + // A basic state is a hard requirement for Voice. + if s.State == nil { + state := NewState() + state.TrackChannels = false + state.TrackEmojis = false + state.TrackMembers = false + state.TrackRoles = false + state.TrackVoice = false + s.State = state + } + if s.wsConn != nil { err = errors.New("Web socket already opened.") return From 0115c9c335929c11d6964dd1072e3c1142fdd2ae Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 27 Sep 2016 22:36:38 -0700 Subject: [PATCH 3/4] Clean up state access. --- wsapi.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wsapi.go b/wsapi.go index 719cd8f..54a420e 100644 --- a/wsapi.go +++ b/wsapi.go @@ -522,13 +522,13 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { return } - // We only care about events that are about us - if se.State.Ready.User.ID != st.UserID { + // We only care about events that are about us. + if se.State.User.ID != st.UserID { return } // Store the SessionID for later use. - voice.UserID = st.UserID // TODO: Review + voice.UserID = st.UserID voice.sessionID = st.SessionID } From ee3e50074964950907fba2fcd9c655ff8c943f61 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 27 Oct 2016 23:43:24 -0700 Subject: [PATCH 4/4] Add webhook support. --- endpoints.go | 6 ++ message.go | 88 +++++++++++++++++++------- restapi.go | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++- state.go | 2 +- structs.go | 33 +++++++++- 5 files changed, 274 insertions(+), 26 deletions(-) diff --git a/endpoints.go b/endpoints.go index 682433d..c79cfe6 100644 --- a/endpoints.go +++ b/endpoints.go @@ -24,6 +24,7 @@ var ( EndpointChannels = EndpointAPI + "channels/" EndpointUsers = EndpointAPI + "users/" EndpointGateway = EndpointAPI + "gateway" + EndpointWebhooks = EndpointAPI + "webhooks/" EndpointAuth = EndpointAPI + "auth/" EndpointLogin = EndpointAuth + "login" @@ -73,6 +74,7 @@ var ( EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" } EndpointGuildIcon = func(gID, hash string) string { return EndpointGuilds + gID + "/icons/" + hash + ".jpg" } EndpointGuildSplash = func(gID, hash string) string { return EndpointGuilds + gID + "/splashes/" + hash + ".jpg" } + EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } EndpointChannel = func(cID string) string { return EndpointChannels + cID } EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } @@ -85,6 +87,10 @@ var ( EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk_delete" } EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } + EndpointChannelWebhooks = func(cID string) string { return EndpointChannel(cID) + "/webhooks" } + + EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } + EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID } diff --git a/message.go b/message.go index 9e33dd2..7b85973 100644 --- a/message.go +++ b/message.go @@ -41,31 +41,73 @@ type MessageAttachment struct { Size int `json:"size"` } +// MessageEmbedFooter is a part of a MessageEmbed struct. +type MessageEmbedFooter struct { + Text string `json:"text,omitempty"` + IconURL string `json:"icon_url,omitempty"` + ProxyIconURL string `json:"proxy_icon_url,omitempty"` +} + +// MessageEmbedImage is a part of a MessageEmbed struct. +type MessageEmbedImage struct { + URL string `json:"url,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// MessageEmbedThumbnail is a part of a MessageEmbed struct. +type MessageEmbedThumbnail struct { + URL string `json:"url,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// MessageEmbedVideo is a part of a MessageEmbed struct. +type MessageEmbedVideo struct { + URL string `json:"url,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// MessageEmbedProvider is a part of a MessageEmbed struct. +type MessageEmbedProvider struct { + URL string `json:"url,omitempty"` + Name string `json:"name,omitempty"` +} + +// MessageEmbedAuthor is a part of a MessageEmbed struct. +type MessageEmbedAuthor struct { + URL string `json:"url,omitempty"` + Name string `json:"name,omitempty"` + IconURL string `json:"icon_url,omitempty"` + ProxyIconURL string `json:"proxy_icon_url,omitempty"` +} + +// MessageEmbedField is a part of a MessageEmbed struct. +type MessageEmbedField struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` + Inline bool `json:"inline,omitempty"` +} + // An MessageEmbed stores data for message embeds. type MessageEmbed struct { - URL string `json:"url"` - Type string `json:"type"` - Title string `json:"title"` - Description string `json:"description"` - Thumbnail *struct { - URL string `json:"url"` - ProxyURL string `json:"proxy_url"` - Width int `json:"width"` - Height int `json:"height"` - } `json:"thumbnail"` - Provider *struct { - URL string `json:"url"` - Name string `json:"name"` - } `json:"provider"` - Author *struct { - URL string `json:"url"` - Name string `json:"name"` - } `json:"author"` - Video *struct { - URL string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` - } `json:"video"` + URL string `json:"url,omitempty"` + Type string `json:"type,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Color int `json:"color,omitempty"` + Footer *MessageEmbedFooter `json:"footer,omitempty"` + Image *MessageEmbedImage `json:"image,omitempty"` + Thumbnail *MessageEmbedThumbnail `json:"thumbnail,omitempty"` + Video *MessageEmbedVideo `json:"video,omitempty"` + Provider *MessageEmbedProvider `json:"provider,omitempty"` + Author *MessageEmbedAuthor `json:"author,omitempty"` + Fields []*MessageEmbedField `json:"fields,omitempty"` } // ContentWithMentionsReplaced will replace all @ mentions with the diff --git a/restapi.go b/restapi.go index 5ecb48f..f561b8a 100644 --- a/restapi.go +++ b/restapi.go @@ -440,7 +440,7 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions } } - if apermissions&PermissionManageRoles > 0 { + if apermissions&PermissionAdministrator > 0 { apermissions |= PermissionAll } @@ -1439,3 +1439,172 @@ func (s *Session) Gateway() (gateway string, err error) { gateway = temp.URL return } + +// Functions specific to Webhooks + +// WebhookCreate returns a new Webhook. +// channelID: The ID of a Channel. +// name : The name of the webhook. +// avatar : The avatar of the webhook. +func (s *Session) WebhookCreate(channelID, name, avatar string) (st *Webhook, err error) { + + data := struct { + Name string `json:"name"` + Avatar string `json:"avatar,omitempty"` + }{name, avatar} + + body, err := s.Request("POST", EndpointChannelWebhooks(channelID), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// ChannelWebhooks returns all webhooks for a given channel. +// channelID: The ID of a channel. +func (s *Session) ChannelWebhooks(channelID string) (st []*Webhook, err error) { + + body, err := s.Request("GET", EndpointChannelWebhooks(channelID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildWebhooks returns all webhooks for a given guild. +// guildID: The ID of a Guild. +func (s *Session) GuildWebhooks(guildID string) (st []*Webhook, err error) { + + body, err := s.Request("GET", EndpointGuildWebhooks(guildID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// Webhook returns a webhook for a given ID +// webhookID: The ID of a webhook. +func (s *Session) Webhook(webhookID string) (st *Webhook, err error) { + + body, err := s.Request("GET", EndpointWebhook(webhookID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookWithToken returns a webhook for a given ID +// webhookID: The ID of a webhook. +// token : The auth token for the webhook. +func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err error) { + + body, err := s.Request("GET", EndpointWebhookToken(webhookID, token), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookEdit updates an existing Webhook. +// webhookID: The ID of a webhook. +// name : The name of the webhook. +// avatar : The avatar of the webhook. +func (s *Session) WebhookEdit(webhookID, name, avatar string) (st *Role, err error) { + + data := struct { + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + }{name, avatar} + + body, err := s.Request("PATCH", EndpointWebhook(webhookID), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookEditWithToken updates an existing Webhook with an auth token. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook. +// name : The name of the webhook. +// avatar : The avatar of the webhook. +func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string) (st *Role, err error) { + + data := struct { + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + }{name, avatar} + + body, err := s.Request("PATCH", EndpointWebhookToken(webhookID, token), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookDelete deletes a webhook for a given ID +// webhookID: The ID of a webhook. +func (s *Session) WebhookDelete(webhookID string) (st *Webhook, err error) { + + body, err := s.Request("DELETE", EndpointWebhook(webhookID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookDeleteWithToken deletes a webhook for a given ID with an auth token. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook. +func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook, err error) { + + body, err := s.Request("DELETE", EndpointWebhookToken(webhookID, token), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookExecute executes a webhook. +// webhookID: The ID of a webhook. +// token : The auth token for the bebhook +func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (err error) { + uri := EndpointWebhookToken(webhookID, token) + + if wait { + uri += "?wait=true" + } + + fmt.Println(uri) + + _, err = s.Request("POST", uri, data) + + return +} diff --git a/state.go b/state.go index aa7997e..5b6ed07 100644 --- a/state.go +++ b/state.go @@ -755,7 +755,7 @@ func (s *State) UserChannelPermissions(userID, channelID string) (apermissions i } } - if apermissions&PermissionManageRoles > 0 { + if apermissions&PermissionAdministrator > 0 { apermissions |= PermissionAll } diff --git a/structs.go b/structs.go index dcfb447..62c5aec 100644 --- a/structs.go +++ b/structs.go @@ -489,6 +489,27 @@ type ChannelPinsUpdate struct { ChannelID string `json:"channel_id"` } +// Webhook stores the data for a webhook. +type Webhook struct { + ID string `json:"id"` + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` + User *User `json:"user"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Token string `json:"token"` +} + +// WebhookParams is a struct for webhook params, used in the WebhookExecute command. +type WebhookParams struct { + Content string `json:"content,omitempty"` + Username string `json:"username,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + TTS bool `json:"tts,omitempty"` + File string `json:"file,omitempty"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` +} + // Constants for the different bit offsets of text channel permissions const ( PermissionReadMessages = 1 << (iota + 10) @@ -499,6 +520,7 @@ const ( PermissionAttachFiles PermissionReadMessageHistory PermissionMentionEveryone + PermissionUseExternalEmojis ) // Constants for the different bit offsets of voice permissions @@ -511,12 +533,21 @@ const ( PermissionVoiceUseVAD ) +// Constants for general management. +const ( + PermissionChangeNickname = 1 << (iota + 26) + PermissionManageNicknames + PermissionManageRoles + PermissionManageWebhooks + PermissionManageEmojis +) + // Constants for the different bit offsets of general permissions const ( PermissionCreateInstantInvite = 1 << iota PermissionKickMembers PermissionBanMembers - PermissionManageRoles + PermissionAdministrator PermissionManageChannels PermissionManageServer