mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-11-05 06:26:08 +00:00
236 lines
5.6 KiB
Go
236 lines
5.6 KiB
Go
// Package voice handles the Discord voice gateway and UDP connections, as well
|
|
// as managing and keeping track of multiple voice sessions.
|
|
//
|
|
// This package abstracts the subpackage voice/voicesession and voice/udp.
|
|
package voice
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/diamondburned/arikawa/v2/discord"
|
|
"github.com/diamondburned/arikawa/v2/gateway"
|
|
"github.com/diamondburned/arikawa/v2/state"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var (
|
|
// defaultErrorHandler is the default error handler
|
|
defaultErrorHandler = func(err error) { log.Println("voice gateway error:", err) }
|
|
|
|
// ErrCannotSend is an error when audio is sent to a closed channel.
|
|
ErrCannotSend = errors.New("cannot send audio to closed channel")
|
|
)
|
|
|
|
// Voice represents a Voice Repository used for managing voice sessions.
|
|
type Voice struct {
|
|
*state.State
|
|
|
|
// Session holds all of the active voice sessions.
|
|
mapmutex sync.Mutex
|
|
sessions map[discord.GuildID]*Session
|
|
|
|
// Callbacks to remove the handlers.
|
|
closers []func()
|
|
|
|
// ErrorLog will be called when an error occurs (defaults to log.Println)
|
|
ErrorLog func(err error)
|
|
}
|
|
|
|
// NewFromToken creates a new voice session from the given token.
|
|
func NewFromToken(token string) (*Voice, error) {
|
|
s, err := state.New(token)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to create a new session")
|
|
}
|
|
|
|
return New(s), nil
|
|
}
|
|
|
|
// New creates a new Voice repository wrapped around a state. The function will
|
|
// also automatically add the GuildVoiceStates intent, as that is required.
|
|
//
|
|
// This function will add the Guilds and GuildVoiceStates intents into the state
|
|
// in order to receive the needed events.
|
|
func New(s *state.State) *Voice {
|
|
// Register the voice intents.
|
|
s.Gateway.AddIntents(gateway.IntentGuilds)
|
|
s.Gateway.AddIntents(gateway.IntentGuildVoiceStates)
|
|
return NewWithoutIntents(s)
|
|
}
|
|
|
|
// NewWithoutIntents creates a new Voice repository wrapped around a state
|
|
// without modifying the given Gateway to add intents.
|
|
func NewWithoutIntents(s *state.State) *Voice {
|
|
v := &Voice{
|
|
State: s,
|
|
sessions: make(map[discord.GuildID]*Session),
|
|
ErrorLog: defaultErrorHandler,
|
|
}
|
|
|
|
// Add the required event handlers to the session.
|
|
v.closers = []func(){
|
|
s.AddHandler(v.onVoiceStateUpdate),
|
|
s.AddHandler(v.onVoiceServerUpdate),
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
// onVoiceStateUpdate receives VoiceStateUpdateEvents from the gateway
|
|
// to keep track of the current user's voice state.
|
|
func (v *Voice) onVoiceStateUpdate(e *gateway.VoiceStateUpdateEvent) {
|
|
// Get the current user.
|
|
me, err := v.Me()
|
|
if err != nil {
|
|
v.ErrorLog(err)
|
|
return
|
|
}
|
|
|
|
// Ignore the event if it is an update from another user.
|
|
if me.ID != e.UserID {
|
|
return
|
|
}
|
|
|
|
// Get the stored voice session for the given guild.
|
|
vs, ok := v.GetSession(e.GuildID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Do what we must.
|
|
vs.UpdateState(e)
|
|
|
|
// Remove the connection if the current user has disconnected.
|
|
if e.ChannelID == 0 {
|
|
v.RemoveSession(e.GuildID)
|
|
}
|
|
}
|
|
|
|
// onVoiceServerUpdate receives VoiceServerUpdateEvents from the gateway
|
|
// to manage the current user's voice connections.
|
|
func (v *Voice) onVoiceServerUpdate(e *gateway.VoiceServerUpdateEvent) {
|
|
// Get the stored voice session for the given guild.
|
|
vs, ok := v.GetSession(e.GuildID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Do what we must.
|
|
vs.UpdateServer(e)
|
|
}
|
|
|
|
// GetSession gets a session for a guild with a read lock.
|
|
func (v *Voice) GetSession(guildID discord.GuildID) (*Session, bool) {
|
|
v.mapmutex.Lock()
|
|
defer v.mapmutex.Unlock()
|
|
|
|
// For some reason you cannot just put `return v.sessions[]` and return a bool D:
|
|
conn, ok := v.sessions[guildID]
|
|
return conn, ok
|
|
}
|
|
|
|
// RemoveSession removes a session.
|
|
func (v *Voice) RemoveSession(guildID discord.GuildID) {
|
|
v.mapmutex.Lock()
|
|
ses, ok := v.sessions[guildID]
|
|
if !ok {
|
|
v.mapmutex.Unlock()
|
|
return
|
|
}
|
|
|
|
delete(v.sessions, guildID)
|
|
v.mapmutex.Unlock()
|
|
|
|
// Ensure that the session is disconnected.
|
|
ses.Disconnect()
|
|
}
|
|
|
|
// JoinChannel joins the specified channel in the specified guild.
|
|
func (v *Voice) JoinChannel(cID discord.ChannelID, muted, deafened bool) (*Session, error) {
|
|
c, err := v.Cabinet.Channel(cID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get channel from state")
|
|
}
|
|
|
|
// Get the stored voice session for the given guild.
|
|
conn, ok := v.GetSession(c.GuildID)
|
|
|
|
// Create a new voice session if one does not exist.
|
|
if !ok {
|
|
u, err := v.Me()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get self")
|
|
}
|
|
|
|
conn = NewSession(v.Session, u.ID)
|
|
conn.ErrorLog = v.ErrorLog
|
|
|
|
v.mapmutex.Lock()
|
|
v.sessions[c.GuildID] = conn
|
|
v.mapmutex.Unlock()
|
|
}
|
|
|
|
// Connect.
|
|
return conn, conn.JoinChannel(c.GuildID, cID, muted, deafened)
|
|
}
|
|
|
|
func (v *Voice) Close() error {
|
|
err := &CloseError{
|
|
SessionErrors: make(map[discord.GuildID]error),
|
|
}
|
|
|
|
v.mapmutex.Lock()
|
|
defer v.mapmutex.Unlock()
|
|
|
|
// Remove all callback handlers.
|
|
for _, fn := range v.closers {
|
|
fn()
|
|
}
|
|
|
|
for gID, s := range v.sessions {
|
|
log.Println("closing", gID)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
if dErr := s.DisconnectCtx(ctx); dErr != nil {
|
|
err.SessionErrors[gID] = dErr
|
|
}
|
|
cancel()
|
|
log.Println("closed", gID)
|
|
}
|
|
|
|
err.StateErr = v.State.Close()
|
|
if err.HasError() {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type CloseError struct {
|
|
SessionErrors map[discord.GuildID]error
|
|
StateErr error
|
|
}
|
|
|
|
func (e *CloseError) HasError() bool {
|
|
if e.StateErr != nil {
|
|
return true
|
|
}
|
|
|
|
return len(e.SessionErrors) > 0
|
|
}
|
|
|
|
func (e *CloseError) Error() string {
|
|
if e.StateErr != nil {
|
|
return e.StateErr.Error()
|
|
}
|
|
|
|
if len(e.SessionErrors) < 1 {
|
|
return ""
|
|
}
|
|
|
|
return strconv.Itoa(len(e.SessionErrors)) + " voice sessions returned errors while attempting to disconnect"
|
|
}
|