diff --git a/_examples/verified_roles/main.go b/_examples/verified_roles/main.go new file mode 100644 index 00000000..390c1794 --- /dev/null +++ b/_examples/verified_roles/main.go @@ -0,0 +1,113 @@ +package main + +import ( + "math/rand" + "net/http" + "os" + "strconv" + + "github.com/disgoorg/disgo" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/oauth2" + "github.com/disgoorg/json" + "github.com/disgoorg/log" +) + +var ( + letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + token = os.Getenv("disgo_token") + clientSecret = os.Getenv("disgo_client_secret") + baseURL = os.Getenv("disgo_base_url") + client bot.Client + oAuth2Client oauth2.Client +) + +func main() { + log.SetLevel(log.LevelDebug) + log.Info("starting example...") + log.Infof("disgo %s", disgo.Version) + + var err error + client, err = disgo.New(token) + if err != nil { + log.Panic(err) + } + + _, _ = client.Rest().UpdateApplicationRoleConnectionMetadata(client.ApplicationID(), []discord.ApplicationRoleConnectionMetadata{ + { + Type: discord.ApplicationRoleConnectionMetadataTypeIntegerGreaterThanOrEqual, + Key: "cookies_eaten", + Name: "Cookies Eaten", + Description: "How many cookies have you eaten?", + }, + }) + + oAuth2Client = oauth2.New(client.ApplicationID(), clientSecret) + + mux := http.NewServeMux() + mux.HandleFunc("/verify", handleVerify) + mux.HandleFunc("/callback", handleCallback) + _ = http.ListenAndServe(":6969", mux) +} + +func handleVerify(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, oAuth2Client.GenerateAuthorizationURL(baseURL+"/callback", discord.PermissionsNone, 0, false, discord.OAuth2ScopeIdentify, discord.OAuth2ScopeRoleConnectionsWrite), http.StatusTemporaryRedirect) +} + +func handleCallback(w http.ResponseWriter, r *http.Request) { + var ( + query = r.URL.Query() + code = query.Get("code") + state = query.Get("state") + ) + if code != "" && state != "" { + identifier := randStr(32) + session, err := oAuth2Client.StartSession(code, state, identifier) + if err != nil { + writeError(w, "error while starting session", err) + return + } + + user, err := oAuth2Client.GetUser(session) + if err != nil { + writeError(w, "error while getting user", err) + return + } + + _, err = oAuth2Client.UpdateApplicationRoleConnection(session, client.ApplicationID(), discord.ApplicationRoleConnectionUpdate{ + PlatformName: json.Ptr("Cookie Monster " + user.Username), + PlatformUsername: json.Ptr("Cookie Monster " + user.Tag()), + Metadata: &map[string]string{ + "cookies_eaten": strconv.Itoa(rand.Intn(100)), + }, + }) + if err != nil { + writeError(w, "error while updating role connection", err) + return + } + + metadata, err := oAuth2Client.GetApplicationRoleConnection(session, client.ApplicationID()) + if err != nil { + writeError(w, "error while getting role connection", err) + return + } + + data, _ := json.MarshalIndent(metadata, "", "\t") + _, _ = w.Write([]byte("updated role connection:\n" + string(data))) + + } +} + +func writeError(w http.ResponseWriter, text string, err error) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(text + ": " + err.Error())) +} + +func randStr(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} diff --git a/discord/application.go b/discord/application.go index 106ec07c..8d4e1186 100644 --- a/discord/application.go +++ b/discord/application.go @@ -9,27 +9,28 @@ import ( ) type Application struct { - ID snowflake.ID `json:"id"` - Name string `json:"name"` - Icon *string `json:"icon,omitempty"` - Description string `json:"description"` - RPCOrigins []string `json:"rpc_origins"` - BotPublic bool `json:"bot_public"` - BotRequireCodeGrant bool `json:"bot_require_code_grant"` - TermsOfServiceURL *string `json:"terms_of_service_url,omitempty"` - PrivacyPolicyURL *string `json:"privacy_policy_url,omitempty"` - CustomInstallURL *string `json:"custom_install_url,omitempty"` - InstallParams *InstallParams `json:"install_params"` - Tags []string `json:"tags"` - Owner *User `json:"owner,omitempty"` - Summary string `json:"summary"` - VerifyKey string `json:"verify_key"` - Team *Team `json:"team,omitempty"` - GuildID *snowflake.ID `json:"guild_id,omitempty"` - PrimarySkuID *snowflake.ID `json:"primary_sku_id,omitempty"` - Slug *string `json:"slug,omitempty"` - CoverImage *string `json:"cover_image,omitempty"` - Flags ApplicationFlags `json:"flags,omitempty"` + ID snowflake.ID `json:"id"` + Name string `json:"name"` + Icon *string `json:"icon,omitempty"` + Description string `json:"description"` + RPCOrigins []string `json:"rpc_origins"` + BotPublic bool `json:"bot_public"` + BotRequireCodeGrant bool `json:"bot_require_code_grant"` + TermsOfServiceURL *string `json:"terms_of_service_url,omitempty"` + PrivacyPolicyURL *string `json:"privacy_policy_url,omitempty"` + CustomInstallURL *string `json:"custom_install_url,omitempty"` + RoleConnectionsVerificationURL *string `json:"role_connections_verification_url"` + InstallParams *InstallParams `json:"install_params"` + Tags []string `json:"tags"` + Owner *User `json:"owner,omitempty"` + Summary string `json:"summary"` + VerifyKey string `json:"verify_key"` + Team *Team `json:"team,omitempty"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + PrimarySkuID *snowflake.ID `json:"primary_sku_id,omitempty"` + Slug *string `json:"slug,omitempty"` + CoverImage *string `json:"cover_image,omitempty"` + Flags ApplicationFlags `json:"flags,omitempty"` } func (a Application) IconURL(opts ...CDNOpt) *string { @@ -100,13 +101,14 @@ const ( OAuth2ScopeGuildsMembersRead OAuth2Scope = "guilds.members.read" OAuth2ScopeGDMJoin OAuth2Scope = "gdm.join" - OAuth2ScopeRelationshipsRead OAuth2Scope = "relationships.read" - OAuth2ScopeIdentify OAuth2Scope = "identify" - OAuth2ScopeEmail OAuth2Scope = "email" - OAuth2ScopeConnections OAuth2Scope = "connections" - OAuth2ScopeBot OAuth2Scope = "bot" - OAuth2ScopeMessagesRead OAuth2Scope = "messages.read" - OAuth2ScopeWebhookIncoming OAuth2Scope = "webhook.incoming" + OAuth2ScopeRelationshipsRead OAuth2Scope = "relationships.read" + OAuth2ScopeRoleConnectionsWrite OAuth2Scope = "role_connections.write" + OAuth2ScopeIdentify OAuth2Scope = "identify" + OAuth2ScopeEmail OAuth2Scope = "email" + OAuth2ScopeConnections OAuth2Scope = "connections" + OAuth2ScopeBot OAuth2Scope = "bot" + OAuth2ScopeMessagesRead OAuth2Scope = "messages.read" + OAuth2ScopeWebhookIncoming OAuth2Scope = "webhook.incoming" ) func (s OAuth2Scope) String() string { diff --git a/discord/application_role_connection_metadata.go b/discord/application_role_connection_metadata.go new file mode 100644 index 00000000..825d1029 --- /dev/null +++ b/discord/application_role_connection_metadata.go @@ -0,0 +1,23 @@ +package discord + +type ApplicationRoleConnectionMetadata struct { + Type ApplicationRoleConnectionMetadataType `json:"type"` + Key string `json:"key"` + Name string `json:"name"` + NameLocalizations map[Locale]string `json:"name_localizations,omitempty"` + Description string `json:"description"` + DescriptionLocalizations map[Locale]string `json:"description_localizations,omitempty"` +} + +type ApplicationRoleConnectionMetadataType int + +const ( + ApplicationRoleConnectionMetadataTypeIntegerLessThanOrEqual ApplicationRoleConnectionMetadataType = iota + 1 + ApplicationRoleConnectionMetadataTypeIntegerGreaterThanOrEqual + ApplicationRoleConnectionMetadataTypeIntegerEqual + ApplicationRoleConnectionMetadataTypeIntegerNotEqual + ApplicationRoleConnectionMetadataTypeDateTimeLessThanOrEqual + ApplicationRoleConnectionMetadataTypeDateTimeGreaterThanOrEqual + ApplicationRoleConnectionMetadataTypeBooleanEqual + ApplicationRoleConnectionMetadataTypeBooleanNotEqual +) diff --git a/discord/role.go b/discord/role.go index 83717987..d106df59 100644 --- a/discord/role.go +++ b/discord/role.go @@ -13,6 +13,7 @@ var _ Mentionable = (*Role)(nil) type Role struct { ID snowflake.ID `json:"id"` Name string `json:"name"` + Description *string `json:"description,omitempty"` Color int `json:"color"` Hoist bool `json:"hoist"` Position int `json:"position"` diff --git a/discord/user.go b/discord/user.go index f03be2ef..42f9c63d 100644 --- a/discord/user.go +++ b/discord/user.go @@ -170,3 +170,15 @@ type SelfUserUpdate struct { Username string `json:"username,omitempty"` Avatar *json.Nullable[Icon] `json:"avatar,omitempty"` } + +type ApplicationRoleConnection struct { + PlatformName *string `json:"platform_name"` + PlatformUsername *string `json:"platform_username"` + Metadata map[string]string `json:"metadata"` +} + +type ApplicationRoleConnectionUpdate struct { + PlatformName *string `json:"platform_name,omitempty"` + PlatformUsername *string `json:"platform_username,omitempty"` + Metadata *map[string]string `json:"metadata,omitempty"` +} diff --git a/oauth2/client.go b/oauth2/client.go index ab4c6657..83e0d642 100644 --- a/oauth2/client.go +++ b/oauth2/client.go @@ -55,4 +55,8 @@ type Client interface { GetGuilds(session Session, opts ...rest.RequestOpt) ([]discord.OAuth2Guild, error) // GetConnections returns the discord.Connection(s) the user has connected. This requires the discord.OAuth2ScopeConnections scope in the Session GetConnections(session Session, opts ...rest.RequestOpt) ([]discord.Connection, error) + // GetApplicationRoleConnection returns the discord.ApplicationRoleConnection for the given application. This requires the discord.OAuth2ScopeRoleConnectionsWrite scope in the Session + GetApplicationRoleConnection(session Session, applicationID snowflake.ID, opts ...rest.RequestOpt) (*discord.ApplicationRoleConnection, error) + // UpdateApplicationRoleConnection updates the discord.ApplicationRoleConnection for the given application. This requires the discord.OAuth2ScopeRoleConnectionsWrite scope in the Session + UpdateApplicationRoleConnection(session Session, applicationID snowflake.ID, update discord.ApplicationRoleConnectionUpdate, opts ...rest.RequestOpt) (*discord.ApplicationRoleConnection, error) } diff --git a/oauth2/client_impl.go b/oauth2/client_impl.go index 33902e6f..7d3ba4ab 100644 --- a/oauth2/client_impl.go +++ b/oauth2/client_impl.go @@ -90,41 +90,53 @@ func (c *clientImpl) RefreshSession(identifier string, session Session, opts ... } func (c *clientImpl) GetUser(session Session, opts ...rest.RequestOpt) (*discord.OAuth2User, error) { - if session.Expiration().Before(time.Now()) { - return nil, ErrAccessTokenExpired - } - if !discord.HasScope(discord.OAuth2ScopeIdentify, session.Scopes()...) { - return nil, ErrMissingOAuth2Scope(discord.OAuth2ScopeIdentify) + if err := checkSession(session, discord.OAuth2ScopeIdentify); err != nil { + return nil, err } return c.Rest().GetCurrentUser(session.AccessToken(), opts...) } func (c *clientImpl) GetMember(session Session, guildID snowflake.ID, opts ...rest.RequestOpt) (*discord.Member, error) { - if session.Expiration().Before(time.Now()) { - return nil, ErrAccessTokenExpired - } - if !discord.HasScope(discord.OAuth2ScopeGuildsMembersRead, session.Scopes()...) { - return nil, ErrMissingOAuth2Scope(discord.OAuth2ScopeGuildsMembersRead) + if err := checkSession(session, discord.OAuth2ScopeGuildsMembersRead); err != nil { + return nil, err } return c.Rest().GetCurrentMember(session.AccessToken(), guildID, opts...) } func (c *clientImpl) GetGuilds(session Session, opts ...rest.RequestOpt) ([]discord.OAuth2Guild, error) { - if session.Expiration().Before(time.Now()) { - return nil, ErrAccessTokenExpired - } - if !discord.HasScope(discord.OAuth2ScopeGuilds, session.Scopes()...) { - return nil, ErrMissingOAuth2Scope(discord.OAuth2ScopeGuilds) + if err := checkSession(session, discord.OAuth2ScopeGuilds); err != nil { + return nil, err } return c.Rest().GetCurrentUserGuilds(session.AccessToken(), 0, 0, 0, opts...) } func (c *clientImpl) GetConnections(session Session, opts ...rest.RequestOpt) ([]discord.Connection, error) { + if err := checkSession(session, discord.OAuth2ScopeConnections); err != nil { + return nil, err + } + return c.Rest().GetCurrentUserConnections(session.AccessToken(), opts...) +} + +func (c *clientImpl) GetApplicationRoleConnection(session Session, applicationID snowflake.ID, opts ...rest.RequestOpt) (*discord.ApplicationRoleConnection, error) { + if err := checkSession(session, discord.OAuth2ScopeRoleConnectionsWrite); err != nil { + return nil, err + } + return c.Rest().GetCurrentUserApplicationRoleConnection(session.AccessToken(), applicationID, opts...) +} + +func (c *clientImpl) UpdateApplicationRoleConnection(session Session, applicationID snowflake.ID, update discord.ApplicationRoleConnectionUpdate, opts ...rest.RequestOpt) (*discord.ApplicationRoleConnection, error) { + if err := checkSession(session, discord.OAuth2ScopeRoleConnectionsWrite); err != nil { + return nil, err + } + return c.Rest().UpdateCurrentUserApplicationRoleConnection(session.AccessToken(), applicationID, update, opts...) +} + +func checkSession(session Session, scope discord.OAuth2Scope) error { if session.Expiration().Before(time.Now()) { - return nil, ErrAccessTokenExpired + return ErrAccessTokenExpired } - if !discord.HasScope(discord.OAuth2ScopeConnections, session.Scopes()...) { - return nil, ErrMissingOAuth2Scope(discord.OAuth2ScopeConnections) + if !discord.HasScope(scope, session.Scopes()...) { + return ErrMissingOAuth2Scope(scope) } - return c.Rest().GetCurrentUserConnections(session.AccessToken(), opts...) + return nil } diff --git a/rest/applications.go b/rest/applications.go index d20d425e..c11e411c 100644 --- a/rest/applications.go +++ b/rest/applications.go @@ -28,6 +28,9 @@ type Applications interface { GetGuildCommandsPermissions(applicationID snowflake.ID, guildID snowflake.ID, opts ...RequestOpt) ([]discord.ApplicationCommandPermissions, error) GetGuildCommandPermissions(applicationID snowflake.ID, guildID snowflake.ID, commandID snowflake.ID, opts ...RequestOpt) (*discord.ApplicationCommandPermissions, error) + + GetApplicationRoleConnectionMetadata(applicationID snowflake.ID, opts ...RequestOpt) ([]discord.ApplicationRoleConnectionMetadata, error) + UpdateApplicationRoleConnectionMetadata(applicationID snowflake.ID, newRecords []discord.ApplicationRoleConnectionMetadata, opts ...RequestOpt) ([]discord.ApplicationRoleConnectionMetadata, error) } type applicationsImpl struct { @@ -142,6 +145,16 @@ func (s *applicationsImpl) GetGuildCommandPermissions(applicationID snowflake.ID return } +func (s *applicationsImpl) GetApplicationRoleConnectionMetadata(applicationID snowflake.ID, opts ...RequestOpt) (metadata []discord.ApplicationRoleConnectionMetadata, err error) { + err = s.client.Do(GetApplicationRoleConnectionMetadata.Compile(nil, applicationID), nil, &metadata, opts...) + return +} + +func (s *applicationsImpl) UpdateApplicationRoleConnectionMetadata(applicationID snowflake.ID, newRecords []discord.ApplicationRoleConnectionMetadata, opts ...RequestOpt) (metadata []discord.ApplicationRoleConnectionMetadata, err error) { + err = s.client.Do(UpdateApplicationRoleConnectionMetadata.Compile(nil, applicationID), newRecords, &metadata, opts...) + return +} + func unmarshalApplicationCommandsToApplicationCommands(unmarshalCommands []discord.UnmarshalApplicationCommand) []discord.ApplicationCommand { commands := make([]discord.ApplicationCommand, len(unmarshalCommands)) for i := range unmarshalCommands { diff --git a/rest/oauth2.go b/rest/oauth2.go index d5fe8623..94203996 100644 --- a/rest/oauth2.go +++ b/rest/oauth2.go @@ -25,6 +25,9 @@ type OAuth2 interface { SetGuildCommandPermissions(bearerToken string, applicationID snowflake.ID, guildID snowflake.ID, commandID snowflake.ID, commandPermissions []discord.ApplicationCommandPermission, opts ...RequestOpt) (*discord.ApplicationCommandPermissions, error) + GetCurrentUserApplicationRoleConnection(bearerToken string, applicationID snowflake.ID, opts ...RequestOpt) (*discord.ApplicationRoleConnection, error) + UpdateCurrentUserApplicationRoleConnection(bearerToken string, applicationID snowflake.ID, connectionUpdate discord.ApplicationRoleConnectionUpdate, opts ...RequestOpt) (*discord.ApplicationRoleConnection, error) + GetAccessToken(clientID snowflake.ID, clientSecret string, code string, redirectURI string, opts ...RequestOpt) (*discord.AccessTokenResponse, error) RefreshAccessToken(clientID snowflake.ID, clientSecret string, refreshToken string, opts ...RequestOpt) (*discord.AccessTokenResponse, error) } @@ -97,6 +100,16 @@ func (s *oAuth2Impl) SetGuildCommandPermissions(bearerToken string, applicationI return } +func (s *oAuth2Impl) GetCurrentUserApplicationRoleConnection(bearerToken string, applicationID snowflake.ID, opts ...RequestOpt) (connection *discord.ApplicationRoleConnection, err error) { + err = s.client.Do(GetCurrentUserApplicationRoleConnection.Compile(nil, applicationID), nil, &connection, withBearerToken(bearerToken, opts)...) + return +} + +func (s *oAuth2Impl) UpdateCurrentUserApplicationRoleConnection(bearerToken string, applicationID snowflake.ID, connectionUpdate discord.ApplicationRoleConnectionUpdate, opts ...RequestOpt) (connection *discord.ApplicationRoleConnection, err error) { + err = s.client.Do(UpdateCurrentUserApplicationRoleConnection.Compile(nil, applicationID), connectionUpdate, &connection, withBearerToken(bearerToken, opts)...) + return +} + func (s *oAuth2Impl) exchangeAccessToken(clientID snowflake.ID, clientSecret string, grantType discord.GrantType, codeOrRefreshToken string, redirectURI string, opts ...RequestOpt) (exchange *discord.AccessTokenResponse, err error) { values := url.Values{ "client_id": []string{clientID.String()}, diff --git a/rest/rest_endpoints.go b/rest/rest_endpoints.go index c63b1be3..bf789506 100644 --- a/rest/rest_endpoints.go +++ b/rest/rest_endpoints.go @@ -35,15 +35,17 @@ var ( // Users var ( - GetUser = NewEndpoint(http.MethodGet, "/users/{user.id}") - GetCurrentUser = NewEndpoint(http.MethodGet, "/users/@me") - GetCurrentMember = NewEndpoint(http.MethodGet, "/users/@me/guilds/{guild.id}/member") - UpdateSelfUser = NewEndpoint(http.MethodPatch, "/users/@me") - GetCurrentUserConnections = NewNoBotAuthEndpoint(http.MethodGet, "/users/@me/connections") - GetCurrentUserGuilds = NewNoBotAuthEndpoint(http.MethodGet, "/users/@me/guilds") - LeaveGuild = NewEndpoint(http.MethodDelete, "/users/@me/guilds/{guild.id}") - GetDMChannels = NewEndpoint(http.MethodGet, "/users/@me/channels") - CreateDMChannel = NewEndpoint(http.MethodPost, "/users/@me/channels") + GetUser = NewEndpoint(http.MethodGet, "/users/{user.id}") + GetCurrentUser = NewEndpoint(http.MethodGet, "/users/@me") + GetCurrentMember = NewEndpoint(http.MethodGet, "/users/@me/guilds/{guild.id}/member") + UpdateSelfUser = NewEndpoint(http.MethodPatch, "/users/@me") + GetCurrentUserConnections = NewNoBotAuthEndpoint(http.MethodGet, "/users/@me/connections") + GetCurrentUserGuilds = NewNoBotAuthEndpoint(http.MethodGet, "/users/@me/guilds") + GetCurrentUserApplicationRoleConnection = NewNoBotAuthEndpoint(http.MethodGet, "/users/@me/applications/{application.id}/role-connection") + UpdateCurrentUserApplicationRoleConnection = NewNoBotAuthEndpoint(http.MethodPut, "/users/@me/applications/{application.id}/role-connection") + LeaveGuild = NewEndpoint(http.MethodDelete, "/users/@me/guilds/{guild.id}") + GetDMChannels = NewEndpoint(http.MethodGet, "/users/@me/channels") + CreateDMChannel = NewEndpoint(http.MethodPost, "/users/@me/channels") ) // Guilds @@ -249,7 +251,7 @@ var ( GetChannelInvites = NewEndpoint(http.MethodGet, "/channels/{channel.id}/invites") ) -// Interactions +// Applications var ( GetGlobalCommands = NewEndpoint(http.MethodGet, "/applications/{application.id}/commands") GetGlobalCommand = NewEndpoint(http.MethodGet, "/applications/{application.id}/command/{command.id}") @@ -278,6 +280,9 @@ var ( CreateFollowupMessage = NewNoBotAuthEndpoint(http.MethodPost, "/webhooks/{application.id}/{interaction.token}") UpdateFollowupMessage = NewNoBotAuthEndpoint(http.MethodPatch, "/webhooks/{application.id}/{interaction.token}/messages/{message.id}") DeleteFollowupMessage = NewNoBotAuthEndpoint(http.MethodDelete, "/webhooks/{application.id}/{interaction.token}/messages/{message.id}") + + GetApplicationRoleConnectionMetadata = NewEndpoint(http.MethodGet, "/applications/{application.id}/role-connections/metadata") + UpdateApplicationRoleConnectionMetadata = NewEndpoint(http.MethodPut, "/applications/{application.id}/role-connections/metadata") ) // NewEndpoint returns a new Endpoint which requires bot auth with the given http method & route.