Skip to content
Closed
73 changes: 73 additions & 0 deletions _examples/rpc/rpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"os"
"os/signal"
"syscall"

"github.com/disgoorg/log"
"github.com/disgoorg/snowflake/v2"

"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/rest"
"github.com/disgoorg/disgo/rpc"
)

var (
clientID = snowflake.GetEnv("disgo_client_id")
clientSecret = os.Getenv("disgo_client_secret")
channelID = snowflake.GetEnv("disgo_channel_id")
)

func main() {
log.SetLevel(log.LevelDebug)
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Info("example is starting...")

oauth2Client := rest.NewOAuth2(rest.NewClient(""))

client, err := rpc.NewClient(clientID)
if err != nil {
log.Fatal(err)
return
}
defer client.Close()

var tokenRs *discord.AccessTokenResponse
if err = client.Send(rpc.Message{
Cmd: rpc.CmdAuthorize,
Args: rpc.CmdArgsAuthorize{
ClientID: clientID,
Scopes: []discord.OAuth2Scope{discord.OAuth2ScopeRPC, discord.OAuth2ScopeMessagesRead},
},
}, rpc.NewHandler(func(data rpc.CmdRsAuthorize) {
tokenRs, err = oauth2Client.GetAccessToken(clientID, clientSecret, data.Code, "http://localhost")
if err != nil {
log.Fatal(err)
}
})); err != nil {
log.Fatal(err)
}

if err = client.Send(rpc.Message{
Cmd: rpc.CmdAuthenticate,
Args: rpc.CmdArgsAuthenticate{
AccessToken: tokenRs.AccessToken,
},
}, nil); err != nil {
log.Fatal(err)
}

if err = client.Subscribe(rpc.EventMessageCreate, rpc.CmdArgsSubscribeMessage{
ChannelID: channelID,
}, rpc.NewHandler(func(data rpc.EventDataMessageCreate) {
log.Info("message: ", data.Message.Content)
})); err != nil {
log.Fatal(err)
}

log.Info("example is now running. Press CTRL-C to exit.")
s := make(chan os.Signal, 1)
signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-s
}
8 changes: 4 additions & 4 deletions discord/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ const (

// Activity represents the fields of a user's presence
type Activity struct {
ID string `json:"id"`
Name string `json:"name"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Type ActivityType `json:"type"`
URL *string `json:"url"`
URL *string `json:"url,omitempty"`
CreatedAt time.Time `json:"created_at"`
Timestamps *ActivityTimestamps `json:"timestamps,omitempty"`
SyncID *string `json:"sync_id,omitempty"`
Expand All @@ -40,7 +40,7 @@ type Activity struct {
Secrets *ActivitySecrets `json:"secrets,omitempty"`
Instance *bool `json:"instance,omitempty"`
Flags ActivityFlags `json:"flags,omitempty"`
Buttons []string `json:"buttons"`
Buttons []string `json:"buttons,omitempty"`
}

func (a *Activity) UnmarshalJSON(data []byte) error {
Expand Down
8 changes: 8 additions & 0 deletions discord/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ type ApplicationUpdate struct {
Tags []string `json:"tags,omitempty"`
}

type OAuth2Application struct {
ID snowflake.ID `json:"id"`
Name string `json:"name"`
Icon *string `json:"icon,omitempty"`
Description string `json:"description"`
RPCOrigins []string `json:"rpc_origins"`
}

type PartialApplication struct {
ID snowflake.ID `json:"id"`
Flags ApplicationFlags `json:"flags"`
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/disgoorg/disgo
go 1.18

require (
github.com/Microsoft/go-winio v0.5.2
github.com/disgoorg/json v1.1.0
github.com/disgoorg/log v1.2.1
github.com/disgoorg/snowflake/v2 v2.0.1
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -13,9 +15,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b h1:qYTY2tN72LhgDj2rtWG+LI6TXFl2ygFQQ4YezfVaGQE=
github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
Expand All @@ -24,6 +28,8 @@ golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
8 changes: 5 additions & 3 deletions rest/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@ func (s *oAuth2Impl) UpdateCurrentUserApplicationRoleConnection(bearerToken stri

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()},
"client_secret": []string{clientSecret},
"grant_type": []string{grantType.String()},
"client_id": []string{clientID.String()},
"grant_type": []string{grantType.String()},
}
if clientSecret != "" {
values["client_secret"] = []string{clientSecret}
}
switch grantType {
case discord.GrantTypeAuthorizationCode:
Expand Down
237 changes: 237 additions & 0 deletions rpc/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package rpc

import (
"bytes"
"context"
"errors"
"io"
"net"
"time"

"github.com/disgoorg/json"
"github.com/disgoorg/log"
"github.com/disgoorg/snowflake/v2"

"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/internal/insecurerandstr"
)

var Version = 1

type TransportCreate func(clientID snowflake.ID, origin string) (Transport, error)

type Transport interface {
NextWriter() (io.WriteCloser, error)
NextReader() (io.Reader, error)
Close() error
}

type Client interface {
Logger() log.Logger
ServerConfig() ServerConfig
User() discord.User
V() int
Transport() Transport

Subscribe(event Event, args CmdArgs, handler Handler) error
Unsubscribe(event Event, args CmdArgs) error
Send(message Message, handler Handler) error
Close()
}

func NewClient(clientID snowflake.ID, opts ...ConfigOpt) (Client, error) {
config := DefaultConfig()
config.Apply(opts)

client := &clientImpl{
logger: config.Logger,
eventHandlers: map[Event]Handler{},
commandHandlers: map[string]internalHandler{},
readyChan: make(chan struct{}, 1),
}

if config.Transport == nil {
var err error
config.Transport, err = config.TransportCreate(clientID, config.Origin)
if err != nil {
return nil, err
}
}
client.transport = config.Transport

go client.listen(client.transport)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-client.readyChan:
}

return client, nil
}

type clientImpl struct {
logger log.Logger
transport Transport

eventHandlers map[Event]Handler
commandHandlers map[string]internalHandler

readyChan chan struct{}
user discord.User
serverConfig ServerConfig
v int
}

func (c *clientImpl) Logger() log.Logger {
return c.logger
}

func (c *clientImpl) ServerConfig() ServerConfig {
return c.serverConfig
}

func (c *clientImpl) User() discord.User {
return c.user
}

func (c *clientImpl) V() int {
return c.v
}

func (c *clientImpl) Transport() Transport {
return c.transport
}

func (c *clientImpl) send(r io.Reader) error {
writer, err := c.transport.NextWriter()
if err != nil {
return err
}
defer writer.Close()

buff := new(bytes.Buffer)
newWriter := io.MultiWriter(writer, buff)

_, err = io.Copy(newWriter, r)
if err != nil {
return err
}

data, _ := io.ReadAll(buff)
c.logger.Tracef("Sending message: data: %s", string(data))

return err
}

func (c *clientImpl) Subscribe(event Event, args CmdArgs, handler Handler) error {
if _, ok := c.eventHandlers[event]; ok {
return errors.New("event already subscribed")
}
c.eventHandlers[event] = handler
return c.Send(Message{
Cmd: CmdSubscribe,
Args: args,
Event: event,
}, nil)
}

func (c *clientImpl) Unsubscribe(event Event, args CmdArgs) error {
if _, ok := c.eventHandlers[event]; ok {
delete(c.eventHandlers, event)
return c.Send(Message{
Cmd: CmdUnsubscribe,
Args: args,
Event: event,
}, nil)
}
return nil
}

func (c *clientImpl) Send(message Message, handler Handler) error {
nonce := insecurerandstr.RandStr(32)
buff := new(bytes.Buffer)

message.Nonce = nonce
if err := json.NewEncoder(buff).Encode(message); err != nil {
return err
}

errChan := make(chan error, 1)

c.commandHandlers[nonce] = internalHandler{
handler: handler,
errChan: errChan,
}
if err := c.send(buff); err != nil {
delete(c.commandHandlers, nonce)
close(errChan)
return err
}
return <-errChan
}

func (c *clientImpl) listen(transport Transport) {
loop:
for {
reader, err := transport.NextReader()
if errors.Is(err, net.ErrClosed) {
c.logger.Error("Connection closed")
break loop
}
if err != nil {
c.logger.Errorf("Error reading message: %s", err)
continue
}

data, err := io.ReadAll(reader)
if err != nil {
c.logger.Errorf("Error reading message: %s", err)
continue
}
c.logger.Tracef("Received message: data: %s", string(data))

reader = bytes.NewReader(data)

var v Message
if err = json.NewDecoder(reader).Decode(&v); err != nil {
c.logger.Errorf("failed to decode message: %s", err)
continue
}

if v.Cmd == CmdDispatch {
if d, ok := v.Data.(EventDataReady); ok {
c.readyChan <- struct{}{}
c.user = d.User
c.serverConfig = d.Config
c.v = d.V
}
if handler, ok := c.eventHandlers[v.Event]; ok {
handler.Handle(v.Data)
}
continue
}
if handler, ok := c.commandHandlers[v.Nonce]; ok {
if v.Event == EventError {
handler.errChan <- v.Data.(EventDataError)
} else {
if handler.handler != nil {
handler.handler.Handle(v.Data)
}
handler.errChan <- nil
}
close(handler.errChan)
delete(c.commandHandlers, v.Nonce)
} else {
c.logger.Errorf("No handler for nonce: %s", v.Nonce)
}
}
}

func (c *clientImpl) Close() {
if c.transport != nil {
_ = c.transport.Close()
}
}
Loading