dndmusicbot/discord/discord.go

273 lines
5.5 KiB
Go
Raw Permalink Normal View History

2022-11-18 21:18:12 +00:00
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
}