arikawa/voice/voice.go

167 lines
4.0 KiB
Go

// Package voice is coming soon to an arikawa near you!
package voice
import (
"log"
"sync"
"time"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/arikawa/state"
"github.com/diamondburned/arikawa/utils/wsutil"
"github.com/pkg/errors"
)
const (
// Version represents the current version of the Discord Voice Gateway this package uses.
Version = "4"
// WSTimeout is the timeout for connecting and writing to the Websocket,
// before Gateway cancels and fails.
WSTimeout = wsutil.DefaultTimeout
)
var (
// defaultErrorHandler is the default error handler
defaultErrorHandler = func(err error) { log.Println("Voice gateway error:", err) }
// WSDebug is used for extra debug logging. This is expected to behave
// similarly to log.Println().
WSDebug = func(v ...interface{}) {}
// ErrMissingForIdentify .
ErrMissingForIdentify = errors.New("missing GuildID, UserID, SessionID, or Token for identify")
// ErrMissingForResume .
ErrMissingForResume = errors.New("missing GuildID, SessionID, or Token for resuming")
)
// Voice .
type Voice struct {
mut sync.RWMutex
state *state.State
// Connections holds all of the active voice connections.
connections map[discord.Snowflake]*Connection
// ErrorLog will be called when an error occurs (defaults to log.Println)
ErrorLog func(err error)
}
// NewVoice creates a new Voice repository.
func NewVoice(s *state.State) *Voice {
v := &Voice{
state: s,
connections: make(map[discord.Snowflake]*Connection),
ErrorLog: defaultErrorHandler,
}
// Add the required event handlers to the session.
s.AddHandler(v.onVoiceStateUpdate)
s.AddHandler(v.onVoiceServerUpdate)
return v
}
// GetConnection gets a connection for a guild with a read lock.
func (v *Voice) GetConnection(guildID discord.Snowflake) (*Connection, bool) {
v.mut.RLock()
defer v.mut.RUnlock()
// For some reason you cannot just put `return v.connections[]` and return a bool D:
conn, ok := v.connections[guildID]
return conn, ok
}
// RemoveConnection removes a connection.
func (v *Voice) RemoveConnection(guildID discord.Snowflake) {
v.mut.Lock()
defer v.mut.Unlock()
delete(v.connections, guildID)
}
// JoinChannel .
func (v *Voice) JoinChannel(gID, cID discord.Snowflake, muted, deafened bool) (*Connection, error) {
// Get the stored voice connection for the given guild.
conn, ok := v.GetConnection(gID)
// Create a new voice connection if one does not exist.
if !ok {
conn = newConnection()
v.mut.Lock()
v.connections[gID] = conn
v.mut.Unlock()
}
// Update values on the connection.
conn.mut.Lock()
conn.GuildID = gID
conn.ChannelID = cID
conn.muted = muted
conn.deafened = deafened
conn.mut.Unlock()
// Ensure that if `cID` is zero that it passes null to the update event.
var channelID *discord.Snowflake
if cID != 0 {
channelID = &cID
}
// https://discordapp.com/developers/docs/topics/voice-connections#retrieving-voice-server-information
// Send a Voice State Update event to the gateway.
err := v.state.Gateway.UpdateVoiceState(gateway.UpdateVoiceStateData{
GuildID: gID,
ChannelID: channelID,
SelfMute: muted,
SelfDeaf: deafened,
})
if err != nil {
return nil, errors.Wrap(err, "Failed to send Voice State Update event")
}
// Wait until we are connected.
WSDebug("Waiting for READY.")
// TODO: Find better way to wait for ready event.
// Check if the connection is opened.
for i := 0; i < 50; i++ {
if conn.WS != nil && conn.WS.Conn != nil {
break
}
time.Sleep(50 * time.Millisecond)
}
if conn.WS == nil || conn.WS.Conn == nil {
return nil, errors.Wrap(err, "Failed to wait for connection to open")
}
for i := 0; i < 50; i++ {
if conn.ready.IP != "" {
break
}
time.Sleep(50 * time.Millisecond)
}
if conn.ready.IP == "" {
return nil, errors.New("failed to wait for ready event")
}
if conn.udpConn == nil {
return nil, errors.New("udp connection is not open")
}
WSDebug("Received READY.")
return conn, nil
}