Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/rpc #170

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
73 changes: 73 additions & 0 deletions _examples/rpc/rpc.go
@@ -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
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
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
Expand Up @@ -3,6 +3,7 @@ module github.com/disgoorg/disgo
go 1.18

require (
github.com/Microsoft/go-winio v0.5.2
topi314 marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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
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
@@ -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()
}
}