Skip to content

Commit

Permalink
Role connections (bwmarrin#1295)
Browse files Browse the repository at this point in the history
* feat: role connection metadata

* feat: add role connection endpoints

Add User Application Role Connection endpoints:
* Get User Application Role Connection
* Update User Application Role Connection

* feat: add example

Add basic example to showcase linked roles flow.

* refactor(endpoints): move role connection metadata

Move Application Role Connection Metadata endpoint to other Application
endpoints.
  • Loading branch information
FedorLap2006 authored Dec 25, 2022
1 parent 4074561 commit 4eef78e
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 7 deletions.
16 changes: 9 additions & 7 deletions endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,12 @@ var (
return EndpointCDNBanners + uID + "/" + cID + ".gif"
}

EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
EndpointUserGuildMember = func(uID, gID string) string { return EndpointUserGuild(uID, gID) + "/member" }
EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" }
EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" }
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID }
EndpointUserGuildMember = func(uID, gID string) string { return EndpointUserGuild(uID, gID) + "/member" }
EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" }
EndpointUserApplicationRoleConnection = func(aID string) string { return EndpointUsers + "@me/applications/" + aID + "/role-connection" }
EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" }

EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
EndpointGuildAutoModeration = func(gID string) string { return EndpointGuild(gID) + "/auto-moderation" }
Expand Down Expand Up @@ -197,8 +198,9 @@ var (
EndpointEmoji = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".png" }
EndpointEmojiAnimated = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".gif" }

EndpointApplications = EndpointAPI + "applications"
EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID }
EndpointApplications = EndpointAPI + "applications"
EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID }
EndpointApplicationRoleConnectionMetadata = func(aID string) string { return EndpointApplication(aID) + "/role-connections/metadata" }

EndpointOAuth2 = EndpointAPI + "oauth2/"
EndpointOAuth2Applications = EndpointOAuth2 + "applications"
Expand Down
11 changes: 11 additions & 0 deletions examples/linked_roles/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/bwmarrin/discordgo/examples/linked_roles

go 1.13

replace github.com/bwmarrin/discordgo v0.26.1 => ../../

require (
github.com/bwmarrin/discordgo v0.26.1
github.com/joho/godotenv v1.4.0
golang.org/x/oauth2 v0.3.0
)
54 changes: 54 additions & 0 deletions examples/linked_roles/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
133 changes: 133 additions & 0 deletions examples/linked_roles/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"net/http"
"net/url"

"github.com/bwmarrin/discordgo"
"github.com/joho/godotenv"
"golang.org/x/oauth2"
)

var oauthConfig = oauth2.Config{
Endpoint: oauth2.Endpoint{
AuthURL: "https://discord.com/oauth2/authorize",
TokenURL: "https://discord.com/api/oauth2/token",
},
Scopes: []string{"identify", "role_connections.write"},
}

var (
appID = flag.String("app", "", "Application ID")
token = flag.String("token", "", "Application token")
clientSecret = flag.String("secret", "", "OAuth2 secret")
redirectURL = flag.String("redirect", "", "OAuth2 Redirect URL")
)

func init() {
flag.Parse()
godotenv.Load()
oauthConfig.ClientID = *appID
oauthConfig.ClientSecret = *clientSecret
oauthConfig.RedirectURL, _ = url.JoinPath(*redirectURL, "/linked-roles-callback")
}

func main() {
s, _ := discordgo.New("Bot " + *token)

_, err := s.ApplicationRoleConnectionMetadataUpdate(*appID, []*discordgo.ApplicationRoleConnectionMetadata{
{
Type: discordgo.ApplicationRoleConnectionMetadataIntegerGreaterThanOrEqual,
Key: "loc",
Name: "Lines of Code",
NameLocalizations: map[discordgo.Locale]string{},
Description: "Total lines of code written",
DescriptionLocalizations: map[discordgo.Locale]string{},
},
{
Type: discordgo.ApplicationRoleConnectionMetadataBooleanEqual,
Key: "gopher",
Name: "Gopher",
NameLocalizations: map[discordgo.Locale]string{},
Description: "Writes in Go",
DescriptionLocalizations: map[discordgo.Locale]string{},
},
{
Type: discordgo.ApplicationRoleConnectionMetadataDatetimeGreaterThanOrEqual,
Key: "first_line",
Name: "First line written",
NameLocalizations: map[discordgo.Locale]string{},
Description: "Days since the first line of code",
DescriptionLocalizations: map[discordgo.Locale]string{},
},
})
if err != nil {
panic(err)
}

fmt.Println("Updated application metadata")
http.HandleFunc("/linked-roles", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
// Redirect the user to Discord OAuth2 page.
http.Redirect(w, r, oauthConfig.AuthCodeURL("random-state"), http.StatusMovedPermanently)
})
http.HandleFunc("/linked-roles-callback", func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
// A safeguard against CSRF attacks.
// Usually tied to requesting user or random.
// NOTE: Hardcoded for the sake of the example.
if q["state"][0] != "random-state" {
return
}

// Fetch the tokens with code we've received.
tokens, err := oauthConfig.Exchange(r.Context(), q["code"][0])
if err != nil {
w.Write([]byte(err.Error()))
return
}

// Construct a temporary session with user's OAuth2 access_token.
ts, _ := discordgo.New("Bearer " + tokens.AccessToken)

// Retrive the user data.
u, err := ts.User("@me")
if err != nil {
w.Write([]byte(err.Error()))
return
}

// Fetch external metadata...
// NOTE: Hardcoded for the sake of the example.
metadata := map[string]string{
"gopher": "1", // 1 for true, 0 for false
"loc": "10000",
"first_line": "1970-01-01", // YYYY-MM-DD
}

// And submit it back to discord.
_, err = ts.UserApplicationRoleConnectionUpdate(*appID, &discordgo.ApplicationRoleConnection{
PlatformName: "Discord Gophers",
PlatformUsername: u.Username,
Metadata: metadata,
})
if err != nil {
w.Write([]byte(err.Error()))
return
}

// Retrieve it to check if everything is ok.
info, err := ts.UserApplicationRoleConnection(*appID)
if err != nil {
w.Write([]byte(err.Error()))
return
}
jsonMetadata, _ := json.Marshal(info.Metadata)
// And show it to the user.
w.Write([]byte(fmt.Sprintf("Your updated metadata is: %s", jsonMetadata)))
})
http.ListenAndServe(":8010", nil)
}
59 changes: 59 additions & 0 deletions restapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -3245,3 +3245,62 @@ func (s *Session) AutoModerationRuleDelete(guildID, ruleID string) (err error) {
_, err = s.RequestWithBucketID("DELETE", endpoint, nil, endpoint)
return
}

// ApplicationRoleConnectionMetadata returns application role connection metadata.
// appID : ID of the application
func (s *Session) ApplicationRoleConnectionMetadata(appID string) (st []*ApplicationRoleConnectionMetadata, err error) {
endpoint := EndpointApplicationRoleConnectionMetadata(appID)
var body []byte
body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
if err != nil {
return
}

err = unmarshal(body, &st)
return
}

// ApplicationRoleConnectionMetadataUpdate updates and returns application role connection metadata.
// appID : ID of the application
// metadata : New metadata
func (s *Session) ApplicationRoleConnectionMetadataUpdate(appID string, metadata []*ApplicationRoleConnectionMetadata) (st []*ApplicationRoleConnectionMetadata, err error) {
endpoint := EndpointApplicationRoleConnectionMetadata(appID)
var body []byte
body, err = s.RequestWithBucketID("PUT", endpoint, metadata, endpoint)
if err != nil {
return
}

err = unmarshal(body, &st)
return
}

// UserApplicationRoleConnection returns user role connection to the specified application.
// appID : ID of the application
func (s *Session) UserApplicationRoleConnection(appID string) (st *ApplicationRoleConnection, err error) {
endpoint := EndpointUserApplicationRoleConnection(appID)
var body []byte
body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint)
if err != nil {
return
}

err = unmarshal(body, &st)
return

}

// UserApplicationRoleConnectionUpdate updates and returns user role connection to the specified application.
// appID : ID of the application
// connection : New ApplicationRoleConnection data
func (s *Session) UserApplicationRoleConnectionUpdate(appID string, rconn *ApplicationRoleConnection) (st *ApplicationRoleConnection, err error) {
endpoint := EndpointUserApplicationRoleConnection(appID)
var body []byte
body, err = s.RequestWithBucketID("PUT", endpoint, rconn, endpoint)
if err != nil {
return
}

err = unmarshal(body, &st)
return
}
32 changes: 32 additions & 0 deletions structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,38 @@ type Application struct {
Flags int `json:"flags,omitempty"`
}

// ApplicationRoleConnectionMetadataType represents the type of application role connection metadata.
type ApplicationRoleConnectionMetadataType int

// Application role connection metadata types.
const (
ApplicationRoleConnectionMetadataIntegerLessThanOrEqual ApplicationRoleConnectionMetadataType = 1
ApplicationRoleConnectionMetadataIntegerGreaterThanOrEqual ApplicationRoleConnectionMetadataType = 2
ApplicationRoleConnectionMetadataIntegerEqual ApplicationRoleConnectionMetadataType = 3
ApplicationRoleConnectionMetadataIntegerNotEqual ApplicationRoleConnectionMetadataType = 4
ApplicationRoleConnectionMetadataDatetimeLessThanOrEqual ApplicationRoleConnectionMetadataType = 5
ApplicationRoleConnectionMetadataDatetimeGreaterThanOrEqual ApplicationRoleConnectionMetadataType = 6
ApplicationRoleConnectionMetadataBooleanEqual ApplicationRoleConnectionMetadataType = 7
ApplicationRoleConnectionMetadataBooleanNotEqual ApplicationRoleConnectionMetadataType = 8
)

// ApplicationRoleConnectionMetadata stores application role connection metadata.
type ApplicationRoleConnectionMetadata struct {
Type ApplicationRoleConnectionMetadataType `json:"type"`
Key string `json:"key"`
Name string `json:"name"`
NameLocalizations map[Locale]string `json:"name_localizations"`
Description string `json:"description"`
DescriptionLocalizations map[Locale]string `json:"description_localizations"`
}

// ApplicationRoleConnection represents the role connection that an application has attached to a user.
type ApplicationRoleConnection struct {
PlatformName string `json:"platform_name"`
PlatformUsername string `json:"platform_username"`
Metadata map[string]string `json:"metadata"`
}

// UserConnection is a Connection returned from the UserConnections endpoint
type UserConnection struct {
ID string `json:"id"`
Expand Down

0 comments on commit 4eef78e

Please sign in to comment.