273 lines
5.5 KiB
Go
273 lines
5.5 KiB
Go
|
package discord
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"time"
|
||
|
|
||
|
"github.com/gorilla/websocket"
|
||
|
"github.com/kataras/go-events"
|
||
|
"github.com/tidwall/gjson"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
GATEWAYBOTURL = "https://discord.com/api/gateway/bot"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
PermissionVoiceConnect = 0x0000000000100000
|
||
|
PermissionVoiceSpeak = 0x0000000000200000
|
||
|
PermissionSendMessages = 0x0000000000000800
|
||
|
)
|
||
|
|
||
|
type Connection struct {
|
||
|
events events.EventEmmiter
|
||
|
httpClient *http.Client
|
||
|
dialer *websocket.Dialer
|
||
|
conn *websocket.Conn
|
||
|
token string
|
||
|
seq int
|
||
|
ready chan bool
|
||
|
vstateUpdate chan bool
|
||
|
vserverUpdate chan bool
|
||
|
}
|
||
|
|
||
|
type VoiceConnection struct {
|
||
|
d *Connection
|
||
|
conn *websocket.Conn
|
||
|
sid string
|
||
|
e string
|
||
|
}
|
||
|
|
||
|
type VoiceStateUpdate struct {
|
||
|
Op int `json:"op"`
|
||
|
Data struct {
|
||
|
Guild string `json:"guild_id"`
|
||
|
Channel string `json:"channel_id"`
|
||
|
Mute bool `json:"self_mute"`
|
||
|
Deaf bool `json:"self_dead"`
|
||
|
} `json:"d"`
|
||
|
}
|
||
|
|
||
|
type Identify struct {
|
||
|
Op int `json:"op"`
|
||
|
|
||
|
Data struct {
|
||
|
Token string `json:"token"`
|
||
|
Intents int `json:"intents"`
|
||
|
|
||
|
Properties struct {
|
||
|
OS string `json:"os"`
|
||
|
Browser string `json:"browser"`
|
||
|
Device string `json:"device"`
|
||
|
} `json:"properties"`
|
||
|
} `json:"d"`
|
||
|
}
|
||
|
|
||
|
func NewConnection(token string) (*Connection, error) {
|
||
|
conn := new(Connection)
|
||
|
|
||
|
conn.token = token
|
||
|
|
||
|
conn.ready = make(chan bool)
|
||
|
conn.vserverUpdate = make(chan bool)
|
||
|
conn.vstateUpdate = make(chan bool)
|
||
|
|
||
|
conn.events = events.New()
|
||
|
conn.events.On("READY", conn.eventready)
|
||
|
|
||
|
conn.dialer = &websocket.Dialer{
|
||
|
HandshakeTimeout: 45 * time.Second,
|
||
|
}
|
||
|
|
||
|
conn.httpClient = &http.Client{}
|
||
|
gw, err := conn.GetGateway()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
conn.conn, _, err = conn.dialer.Dial(gw, nil)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
go func() {
|
||
|
for {
|
||
|
_, b, err := conn.conn.ReadMessage()
|
||
|
if err != nil {
|
||
|
log.Println(err)
|
||
|
break
|
||
|
}
|
||
|
|
||
|
go conn.handleGatewayEvent(b)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Identify
|
||
|
id := new(Identify)
|
||
|
id.Op = 2
|
||
|
id.Data.Token = token
|
||
|
id.Data.Intents = PermissionVoiceConnect | PermissionVoiceSpeak | PermissionSendMessages
|
||
|
id.Data.Properties.OS = "linux"
|
||
|
id.Data.Properties.Browser = "dndmusicbot/0.0.1"
|
||
|
id.Data.Properties.Device = "dndmusicbot"
|
||
|
|
||
|
json.NewEncoder(os.Stdout).Encode(id)
|
||
|
|
||
|
err = conn.conn.WriteJSON(id)
|
||
|
if err != nil {
|
||
|
conn.conn.Close()
|
||
|
return nil, err
|
||
|
}
|
||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
|
defer cancel()
|
||
|
|
||
|
// Wait for Ready!
|
||
|
select {
|
||
|
case <-ctx.Done():
|
||
|
return nil, err
|
||
|
case <-conn.ready:
|
||
|
}
|
||
|
|
||
|
return conn, nil
|
||
|
}
|
||
|
|
||
|
func (c *Connection) NewVoiceConnection(guild string, channel string) (*VoiceConnection, error) {
|
||
|
vc := new(VoiceConnection)
|
||
|
vc.d = c
|
||
|
|
||
|
c.events.On("VOICE_STATE_UPDATE", vc.voiceStateUpdate)
|
||
|
c.events.On("VOICE_SERVER_UDPATE", vc.voiceServerUpdate)
|
||
|
|
||
|
msg := new(VoiceStateUpdate)
|
||
|
msg.Op = 4
|
||
|
msg.Data.Guild = guild
|
||
|
msg.Data.Channel = channel
|
||
|
msg.Data.Mute = false
|
||
|
msg.Data.Deaf = true
|
||
|
|
||
|
c.conn.WriteJSON(msg)
|
||
|
|
||
|
var state, server bool
|
||
|
|
||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
|
defer cancel()
|
||
|
|
||
|
// We have to wait for both a vserverupdate and a vstateupdate before we continue.
|
||
|
for !state && !server {
|
||
|
select {
|
||
|
case <-ctx.Done():
|
||
|
return nil, ctx.Err()
|
||
|
case <-c.vserverUpdate:
|
||
|
server = true
|
||
|
case <-c.vstateUpdate:
|
||
|
state = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We should have gotten all the information by now.. continue..
|
||
|
|
||
|
return vc, nil
|
||
|
}
|
||
|
|
||
|
func (vc *VoiceConnection) voiceStateUpdate(payload ...interface{}) {
|
||
|
vc.d.vstateUpdate <- true
|
||
|
}
|
||
|
|
||
|
func (vc *VoiceConnection) voiceServerUpdate(payload ...interface{}) {
|
||
|
vc.d.vserverUpdate <- true
|
||
|
}
|
||
|
|
||
|
// Ready event. Contains alot of information regarding our session.
|
||
|
func (c *Connection) eventready(payload ...interface{}) {
|
||
|
c.ready <- true
|
||
|
}
|
||
|
|
||
|
func (c *Connection) handleGatewayEvent(js []byte) error {
|
||
|
eventdata := gjson.GetManyBytes(js, "op", "d", "s", "t")
|
||
|
|
||
|
op := eventdata[0]
|
||
|
data := eventdata[1]
|
||
|
seq := eventdata[2]
|
||
|
eventname := eventdata[3]
|
||
|
|
||
|
if seq.Exists() {
|
||
|
c.seq = int(seq.Int())
|
||
|
}
|
||
|
|
||
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
defer cancel()
|
||
|
c.conn.SetCloseHandler(func(code int, text string) error {
|
||
|
cancel()
|
||
|
return nil
|
||
|
})
|
||
|
|
||
|
switch op.Int() {
|
||
|
case 0:
|
||
|
fmt.Printf("Event: %s Data: %+v\n", eventname.String(), data)
|
||
|
c.events.Emit(events.EventName(eventname.String()), data)
|
||
|
case 10: // Hello
|
||
|
hb_interval, err := time.ParseDuration(fmt.Sprintf("%dms", data.Get("heartbeat_interval").Int()))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
log.Printf("We Received a Hello! Starting heartbeat on %.2fs interval\n", hb_interval.Seconds())
|
||
|
|
||
|
hb_ticker := time.NewTicker(hb_interval)
|
||
|
for {
|
||
|
select {
|
||
|
case <-ctx.Done():
|
||
|
break
|
||
|
case <-hb_ticker.C:
|
||
|
msg := make(map[string]interface{})
|
||
|
msg["op"] = 1
|
||
|
if c.seq == 0 {
|
||
|
msg["d"] = nil
|
||
|
} else {
|
||
|
msg["d"] = c.seq
|
||
|
}
|
||
|
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||
|
log.Printf("Sending Heartbeat: %+v\n", msg)
|
||
|
err = c.conn.WriteJSON(msg)
|
||
|
if err != nil {
|
||
|
c.conn.Close()
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
case 11:
|
||
|
log.Println("Heartbeat ACK!")
|
||
|
default:
|
||
|
log.Printf("%d: %+v\n", op.Int(), data)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (c *Connection) GetGateway() (string, error) {
|
||
|
req, err := http.NewRequest("GET", GATEWAYBOTURL, nil)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
req.Header.Add("Authorization", "Bot "+c.token)
|
||
|
|
||
|
resp, err := http.DefaultClient.Do(req)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
b, err := io.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
return gjson.GetBytes(b, "url").String(), err
|
||
|
}
|