diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-20 15:21:37 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-20 15:21:37 +0200 |
| commit | 1f37a67c53233dc8e814d22620e1ff8a6fdb14b6 (patch) | |
| tree | 42062b7e90cda16bd25091ff9c31046d18b6029c /cogs/voice.py | |
| parent | proper context for settings (diff) | |
| download | senator-sperma-1f37a67c53233dc8e814d22620e1ff8a6fdb14b6.tar.gz senator-sperma-1f37a67c53233dc8e814d22620e1ff8a6fdb14b6.zip | |
health checks for vc
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to 'cogs/voice.py')
| -rw-r--r-- | cogs/voice.py | 122 |
1 files changed, 89 insertions, 33 deletions
diff --git a/cogs/voice.py b/cogs/voice.py index 046c2a8..6f9a269 100644 --- a/cogs/voice.py +++ b/cogs/voice.py | |||
| @@ -24,60 +24,116 @@ def get_voice_manager(bot) -> "VoiceManager": | |||
| 24 | return _voice_managers[bot_id] | 24 | return _voice_managers[bot_id] |
| 25 | 25 | ||
| 26 | 26 | ||
| 27 | def is_voice_client_alive(voice_client, guild): | ||
| 28 | """Check if a voice client connection is healthy. | ||
| 29 | |||
| 30 | Verifies the client is connected, the channel still exists, | ||
| 31 | and the underlying voice socket is still open. | ||
| 32 | """ | ||
| 33 | if voice_client is None: | ||
| 34 | return False | ||
| 35 | if not voice_client.is_connected(): | ||
| 36 | return False | ||
| 37 | if voice_client.channel is None: | ||
| 38 | return False | ||
| 39 | if guild.get_channel(voice_client.channel.id) is None: | ||
| 40 | return False | ||
| 41 | |||
| 42 | try: | ||
| 43 | ws = voice_client._connection | ||
| 44 | s = ws.socket | ||
| 45 | if s is None or s.fileno() == -1: | ||
| 46 | return False | ||
| 47 | except Exception: | ||
| 48 | return False | ||
| 49 | |||
| 50 | return True | ||
| 51 | |||
| 52 | |||
| 27 | @dataclass | 53 | @dataclass |
| 28 | class VoiceManager: | 54 | class VoiceManager: |
| 29 | bot: Any # type: ignore[no-any-import] | 55 | bot: Any # type: ignore[no-any-import] |
| 30 | _voice_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False) | 56 | _voice_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False) |
| 57 | _connecting: set[int] = field(default_factory=set, init=False) | ||
| 31 | 58 | ||
| 32 | async def join_channel(self, channel): | 59 | async def join_channel(self, channel): |
| 33 | """Join a voice channel by channel object.""" | 60 | """Join a voice channel by channel object.""" |
| 34 | async with self._voice_lock: | 61 | async with self._voice_lock: |
| 35 | guild = channel.guild | 62 | guild = channel.guild |
| 63 | guild_id = guild.id | ||
| 64 | |||
| 65 | if guild_id in self._connecting: | ||
| 66 | logger.info(f"Connection already in progress for guild {guild_id}, skipping") | ||
| 67 | return None | ||
| 68 | |||
| 36 | voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) | 69 | voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) |
| 37 | 70 | ||
| 38 | if voice_client and voice_client.is_connected(): | 71 | if voice_client and voice_client.is_connected(): |
| 39 | if voice_client.channel == channel: | 72 | if voice_client.channel == channel: |
| 40 | logger.info(f"Already connected to {channel}") | 73 | if is_voice_client_alive(voice_client, guild): |
| 41 | return voice_client | 74 | logger.info(f"Already connected to {channel}") |
| 42 | try: | 75 | return voice_client |
| 43 | await voice_client.move_to(channel) | 76 | logger.warning(f"Stale connection to {channel}, reconnecting") |
| 44 | logger.info(f"Moved to voice channel: {channel}") | ||
| 45 | return voice_client | ||
| 46 | except Exception as e: | ||
| 47 | logger.error(f"Failed to move voice client: {e}") | ||
| 48 | logger.info("Disconnecting after failed move attempt") | ||
| 49 | try: | 77 | try: |
| 50 | await voice_client.disconnect() | 78 | await voice_client.disconnect() |
| 51 | except Exception: | 79 | except Exception: |
| 52 | pass | 80 | pass |
| 53 | 81 | else: | |
| 54 | await asyncio.sleep(0.3) | 82 | try: |
| 55 | try: | 83 | await voice_client.move_to(channel) |
| 56 | voice_client = await channel.connect(timeout=30.0, reconnect=False) | 84 | logger.info(f"Moved to voice channel: {channel}") |
| 57 | logger.info(f"Connected to voice channel: {channel}") | 85 | return voice_client |
| 58 | return voice_client | 86 | except Exception as e: |
| 59 | except discord.ClientException as e: | 87 | logger.error(f"Failed to move voice client: {e}") |
| 60 | if "Already connected" in str(e): | 88 | logger.info("Disconnecting after failed move attempt") |
| 61 | logger.warning(f"Stale voice state detected, forcing disconnect and reconnect: {e}") | ||
| 62 | voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) | ||
| 63 | if voice_client: | ||
| 64 | try: | 89 | try: |
| 65 | await voice_client.disconnect() | 90 | await voice_client.disconnect() |
| 66 | except Exception: | 91 | except Exception: |
| 67 | pass | 92 | pass |
| 68 | await asyncio.sleep(0.3) | 93 | elif voice_client and not voice_client.is_connected(): |
| 69 | try: | 94 | logger.info("Voice client exists but disconnected, cleaning up stale entry") |
| 70 | voice_client = await channel.connect(timeout=30.0, reconnect=False) | 95 | try: |
| 71 | logger.info(f"Reconnected to voice channel: {channel}") | 96 | await voice_client.disconnect() |
| 72 | return voice_client | 97 | except Exception: |
| 73 | except Exception as e2: | 98 | pass |
| 74 | logger.error(f"Failed to reconnect after stale cleanup: {e2}") | 99 | voice_client = None |
| 75 | return None | 100 | |
| 76 | logger.error(f"Failed to connect to voice channel: {e}") | 101 | self._connecting.add(guild_id) |
| 77 | return None | 102 | try: |
| 78 | except Exception as e: | 103 | await asyncio.sleep(0.3) |
| 79 | logger.error(f"Failed to connect to voice channel: {e}") | 104 | try: |
| 80 | return None | 105 | voice_client = await channel.connect(timeout=30.0, reconnect=False) |
| 106 | logger.info(f"Connected to voice channel: {channel}") | ||
| 107 | return voice_client | ||
| 108 | except discord.ClientException as e: | ||
| 109 | if "Already connected" in str(e): | ||
| 110 | logger.warning(f"Stale voice state detected, forcing disconnect and reconnect: {e}") | ||
| 111 | voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) | ||
| 112 | if voice_client: | ||
| 113 | try: | ||
| 114 | await voice_client.disconnect() | ||
| 115 | except Exception: | ||
| 116 | pass | ||
| 117 | else: | ||
| 118 | try: | ||
| 119 | channel._state._remove_voice_client(guild.id) | ||
| 120 | except Exception: | ||
| 121 | pass | ||
| 122 | await asyncio.sleep(0.3) | ||
| 123 | try: | ||
| 124 | voice_client = await channel.connect(timeout=30.0, reconnect=False) | ||
| 125 | logger.info(f"Reconnected to voice channel: {channel}") | ||
| 126 | return voice_client | ||
| 127 | except Exception as e2: | ||
| 128 | logger.error(f"Failed to reconnect after stale cleanup: {e2}") | ||
| 129 | return None | ||
| 130 | logger.error(f"Failed to connect to voice channel: {e}") | ||
| 131 | return None | ||
| 132 | except Exception as e: | ||
| 133 | logger.error(f"Failed to connect to voice channel: {e}") | ||
| 134 | return None | ||
| 135 | finally: | ||
| 136 | self._connecting.discard(guild_id) | ||
| 81 | 137 | ||
| 82 | async def join_by_channel_id(self, channel_id): | 138 | async def join_by_channel_id(self, channel_id): |
| 83 | """Join a voice channel by channel ID.""" | 139 | """Join a voice channel by channel ID.""" |
