|
1 | | -from . import _native_voice as _native |
2 | | - |
3 | | -import discord |
4 | | -import asyncio |
5 | | -import logging |
6 | | - |
7 | | -from discord.backoff import ExponentialBackoff |
8 | | - |
9 | | -log = logging.getLogger(__name__) |
10 | | - |
11 | | -class VoiceClient(discord.VoiceProtocol): |
12 | | - def __init__(self, client, channel): |
13 | | - super().__init__(client, channel) |
14 | | - self._connector = _native.VoiceConnector() |
15 | | - self._connection = None |
16 | | - if client.user is None: |
17 | | - raise ValueError('Client has not been ready and has no client user set up.') |
18 | | - self._connector.user_id = client.user.id |
19 | | - self._voice_state_complete = asyncio.Event() |
20 | | - self._voice_server_complete = asyncio.Event() |
21 | | - self._attempts = 0 |
22 | | - self._runner = None |
23 | | - self.guild = channel.guild |
24 | | - |
25 | | - async def on_voice_state_update(self, data): |
26 | | - self._connector.session_id = data['session_id'] |
27 | | - |
28 | | - if self._connection is not None: |
29 | | - channel_id = data['channel_id'] |
30 | | - if channel_id is None: |
31 | | - return await self.disconnect() |
32 | | - |
33 | | - self.channel = channel_id and self.guild.get_channel(int(chananel_id)) |
34 | | - else: |
35 | | - self._voice_state_complete.set() |
36 | | - |
37 | | - async def on_voice_server_update(self, data): |
38 | | - if self._voice_server_complete.is_set(): |
39 | | - log.info('Ignoring extraneous voice server update.') |
40 | | - return |
41 | | - |
42 | | - token = data.get('token') |
43 | | - server_id = data['guild_id'] |
44 | | - endpoint = data.get('endpoint') |
45 | | - |
46 | | - if endpoint is None or token is None: |
47 | | - log.warning('Awaiting endpoint... This requires waiting. ' \ |
48 | | - 'If timeout occurred considering raising the timeout and reconnecting.') |
49 | | - return |
50 | | - |
51 | | - endpoint, _, _ = endpoint.rpartition(':') |
52 | | - if endpoint.startswith('wss://'): |
53 | | - endpoint = endpoint[6:] |
54 | | - |
55 | | - self._connector.update_socket(token, server_id, endpoint) |
56 | | - self._voice_server_complete.set() |
57 | | - |
58 | | - async def voice_connect(self): |
59 | | - self._attempts += 1 |
60 | | - await self.guild.change_voice_state(channel=self.channel) |
61 | | - |
62 | | - async def voice_disconnect(self): |
63 | | - log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', self.channel.id, self.guild.id) |
64 | | - await self.guild.change_voice_state(channel=None) |
65 | | - |
66 | | - async def connect(self, *, reconnect, timeout): |
67 | | - log.info('Connecting to voice...') |
68 | | - self._voice_state_complete.clear() |
69 | | - self._voice_server_complete.clear() |
70 | | - |
71 | | - # This has to be created before we start the flow. |
72 | | - futures = [ |
73 | | - self._voice_state_complete.wait(), |
74 | | - self._voice_server_complete.wait(), |
75 | | - ] |
76 | | - |
77 | | - # Start the connection flow |
78 | | - log.info('Starting voice handshake... (connection attempt %d)', self._attempts + 1) |
79 | | - await self.voice_connect() |
80 | | - |
81 | | - try: |
82 | | - await discord.utils.sane_wait_for(futures, timeout=timeout) |
83 | | - except asyncio.TimeoutError: |
84 | | - await self.disconnect(force=True) |
85 | | - raise |
86 | | - |
87 | | - log.info('Voice handshake complete. Endpoint found %s', self._connector.endpoint) |
88 | | - self._voice_server_complete.clear() |
89 | | - self._voice_state_complete.clear() |
90 | | - |
91 | | - loop = asyncio.get_running_loop() |
92 | | - self._connection = await self._connector.connect(loop) |
93 | | - if self._runner is not None: |
94 | | - self._runner.cancel() |
95 | | - |
96 | | - self._runner = loop.create_task(self.reconnect_handler(reconnect, timeout)) |
97 | | - |
98 | | - async def reconnect_handler(self, reconnect, timeout): |
99 | | - backoff = ExponentialBackoff() |
100 | | - loop = asyncio.get_running_loop() |
101 | | - |
102 | | - while True: |
103 | | - try: |
104 | | - await self._connection.run(loop) |
105 | | - except _native_voice.ConnectionClosed as e: |
106 | | - log.info('Voice connection got a clean close %s', e) |
107 | | - await self.disconnect() |
108 | | - return |
109 | | - except _native_voice.ConnectionError as e: |
110 | | - log.exception('Internal voice error: %s', e) |
111 | | - await self.disconnect() |
112 | | - return |
113 | | - except (_native_voice.ReconnectError) as e: |
114 | | - if not reconnect: |
115 | | - await self.disconnect() |
116 | | - raise |
117 | | - |
118 | | - retry = backoff.delay() |
119 | | - log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry) |
120 | | - |
121 | | - await asyncio.sleep(retry) |
122 | | - await self.voice_disconnect() |
123 | | - try: |
124 | | - await self.connect(reconnect=True, timeout=timeout) |
125 | | - except asyncio.TimeoutError: |
126 | | - # at this point we've retried 5 times... let's continue the loop. |
127 | | - log.warning('Could not connect to voice... Retrying...') |
128 | | - continue |
129 | | - else: |
130 | | - # The function above is actually a loop already |
131 | | - # So if we're here then it exited normally |
132 | | - await self.disconnect() |
133 | | - return |
134 | | - |
135 | | - async def disconnect(self, *, force=False): |
136 | | - try: |
137 | | - if self._connection is not None: |
138 | | - self._connection.disconnect() |
139 | | - self._connection = None |
140 | | - |
141 | | - await self.voice_disconnect() |
142 | | - finally: |
143 | | - self.cleanup() |
144 | | - |
145 | | - def play(self, title): |
146 | | - if self._connection: |
147 | | - self._connection.play(title) |
148 | | - |
149 | | - def stop(self): |
150 | | - if self._connection: |
151 | | - self._connection.stop() |
152 | | - |
153 | | - def is_playing(self): |
154 | | - if self._connection: |
155 | | - return self._connection.is_playing() |
156 | | - return False |
157 | | - |
158 | | - def _debug_info(self): |
159 | | - if self._connection: |
160 | | - return self._connection.get_state() |
161 | | - return {} |
0 commit comments