-
Notifications
You must be signed in to change notification settings - Fork 4
/
voice_connection.go
161 lines (140 loc) · 4.53 KB
/
voice_connection.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package harmony
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/skwair/harmony/discord"
"github.com/skwair/harmony/internal/payload"
"github.com/skwair/harmony/voice"
)
// JoinVoiceChannel will create a new voice connection to the given voice channel.
// If you already have an existing connection and want to switch to a different channel
// instead, use the SwitchVoiceChannel method.
// This method is safe to call from multiple goroutines, but connections will happen
// sequentially.
// To properly leave the voice channel, call LeaveVoiceChannel.
func (c *Client) JoinVoiceChannel(ctx context.Context, guildID, channelID string, mute, deaf bool) (*voice.Connection, error) {
c.mu.Lock()
defer c.mu.Unlock()
if !c.isConnected() {
return nil, discord.ErrGatewayNotConnected
}
// Check if we already have a voice connection in this guild.
if _, ok := c.voiceConnections[guildID]; ok {
return nil, discord.ErrAlreadyConnectedToVoice
}
// This is used to notify the already started event handler that
// some specific payloads should be sent through to c.payloads.
c.connectingToVoice.Store(true)
defer c.connectingToVoice.Store(false)
// Notify a voice server that we want to connect to a voice channel.
vsu := &voice.StateUpdate{
State: voice.State{
GuildID: guildID,
ChannelID: &channelID,
SelfMute: mute,
SelfDeaf: deaf,
},
}
if err := c.sendPayload(ctx, gatewayOpcodeVoiceStateUpdate, vsu); err != nil {
return nil, err
}
// The voice server should answer with two payloads,
// describing the voice state and the voice server
// to connect to.
state, server, err := getStateAndServer(c.voicePayloads)
if err != nil {
return nil, err
}
// Establish the voice connection.
conn, err := voice.Connect(ctx, state, server, voice.WithLogger(c.logger))
if err != nil {
return nil, err
}
c.voiceConnections[guildID] = conn
return conn, nil
}
// SwitchVoiceChannel can be used to switch from a voice channel to another. It requires an
// active voice connection in the guild. You can get one with JoinVoiceChannel.
func (c *Client) SwitchVoiceChannel(ctx context.Context, guildID string, channelID string) error {
c.mu.Lock()
defer c.mu.Unlock()
conn, ok := c.voiceConnections[guildID]
if !ok {
return discord.ErrNotConnectedToVoice
}
vsu := &voice.StateUpdate{
State: voice.State{
GuildID: guildID,
ChannelID: &channelID,
SelfMute: conn.State().SelfMute,
SelfDeaf: conn.State().SelfDeaf,
},
}
if err := c.sendPayload(ctx, gatewayOpcodeVoiceStateUpdate, vsu); err != nil {
return err
}
return nil
}
// LeaveVoiceChannel notifies the Gateway we want the voice channel we are
// connected to in the given guild.
func (c *Client) LeaveVoiceChannel(ctx context.Context, guildID string) error {
conn, ok := c.voiceConnections[guildID]
if ok {
conn.Close()
delete(c.voiceConnections, guildID)
}
vsu := &voice.StateUpdate{
State: voice.State{
GuildID: guildID,
},
}
if err := c.sendPayload(ctx, gatewayOpcodeVoiceStateUpdate, vsu); err != nil {
return fmt.Errorf("could not send voice state update payload: %w", err)
}
return nil
}
// getStateAndServer will receive exactly two payloads from ch and extract the voice state
// and the voice server information from them. The order of the payloads is not relevant
// although only those two payloads must be sent through ch and only once each.
// NOTE: check if those events are always sequentially sent in the same order, if so,
// refactor this function.
func getStateAndServer(ch chan *payload.Payload) (*voice.StateUpdate, *voice.ServerUpdate, error) {
var (
server voice.ServerUpdate
state voice.StateUpdate
pld *payload.Payload
first, second bool
)
for i := 0; i < 2; i++ {
select {
case pld = <-ch:
case <-time.After(15 * time.Second):
return nil, nil, errors.New("timed out waiting for voice payloads")
}
if pld.T == eventVoiceStateUpdate {
if first {
return nil, nil, errors.New("already received voice state update payload")
}
first = true
if err := json.Unmarshal(pld.D, &state); err != nil {
return nil, nil, err
}
} else if pld.T == eventVoiceServerUpdate {
if second {
return nil, nil, errors.New("already received voice server update payload")
}
second = true
if err := json.Unmarshal(pld.D, &server); err != nil {
return nil, nil, err
}
} else {
return nil, nil, fmt.Errorf(
"expected Opcode 0 VOICE_STATE_UPDATE or VOICE_SERVER_UPDATE; got Opcode %d %s",
pld.Op, pld.T)
}
}
return &state, &server, nil
}