Normalize channel names for join tracking and commands
This commit is contained in:
@@ -95,6 +95,15 @@ class DuckHuntBot:
|
|||||||
else:
|
else:
|
||||||
return default
|
return default
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def _channel_key(self, channel: str) -> str:
|
||||||
|
"""Normalize channel names for internal comparisons (IRC channels are case-insensitive)."""
|
||||||
|
if not isinstance(channel, str):
|
||||||
|
return ""
|
||||||
|
channel = channel.strip()
|
||||||
|
if channel.startswith('#') or channel.startswith('&'):
|
||||||
|
return channel.lower()
|
||||||
|
return channel
|
||||||
|
|
||||||
def is_admin(self, user):
|
def is_admin(self, user):
|
||||||
if '!' not in user:
|
if '!' not in user:
|
||||||
@@ -454,7 +463,7 @@ class DuckHuntBot:
|
|||||||
for channel in channels:
|
for channel in channels:
|
||||||
try:
|
try:
|
||||||
self.send_raw(f"JOIN {channel}")
|
self.send_raw(f"JOIN {channel}")
|
||||||
self.pending_joins[channel] = None
|
self.pending_joins[self._channel_key(channel)] = None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error joining channel {channel}: {e}")
|
self.logger.error(f"Error joining channel {channel}: {e}")
|
||||||
|
|
||||||
@@ -468,8 +477,9 @@ class DuckHuntBot:
|
|||||||
if params and len(params) >= 2 and params[0].lower() == our_nick.lower():
|
if params and len(params) >= 2 and params[0].lower() == our_nick.lower():
|
||||||
failed_channel = params[1]
|
failed_channel = params[1]
|
||||||
reason = trailing or "Join rejected"
|
reason = trailing or "Join rejected"
|
||||||
self.channels_joined.discard(failed_channel)
|
failed_key = self._channel_key(failed_channel)
|
||||||
requester = self.pending_joins.pop(failed_channel, None)
|
self.channels_joined.discard(failed_key)
|
||||||
|
requester = self.pending_joins.pop(failed_key, None)
|
||||||
self.logger.warning(f"Failed to join {failed_channel}: ({command}) {reason}")
|
self.logger.warning(f"Failed to join {failed_channel}: ({command}) {reason}")
|
||||||
if requester:
|
if requester:
|
||||||
try:
|
try:
|
||||||
@@ -495,15 +505,16 @@ class DuckHuntBot:
|
|||||||
|
|
||||||
# Check if we successfully joined (or rejoined) a channel
|
# Check if we successfully joined (or rejoined) a channel
|
||||||
if joiner_nick and joiner_nick.lower() == our_nick.lower():
|
if joiner_nick and joiner_nick.lower() == our_nick.lower():
|
||||||
self.channels_joined.add(channel)
|
channel_key = self._channel_key(channel)
|
||||||
|
self.channels_joined.add(channel_key)
|
||||||
self.logger.info(f"Successfully joined channel {channel}")
|
self.logger.info(f"Successfully joined channel {channel}")
|
||||||
|
|
||||||
# If this was an admin-requested join, persist it now.
|
# If this was an admin-requested join, persist it now.
|
||||||
requester = self.pending_joins.pop(channel, None)
|
requester = self.pending_joins.pop(channel_key, None)
|
||||||
if requester:
|
if requester:
|
||||||
try:
|
try:
|
||||||
channels = self._config_channels_list()
|
channels = self._config_channels_list()
|
||||||
if channel not in channels:
|
if not any(self._channel_key(c) == channel_key for c in channels if isinstance(c, str)):
|
||||||
channels.append(channel)
|
channels.append(channel)
|
||||||
self._persist_config()
|
self._persist_config()
|
||||||
self.send_message(requester, f"{requester} > Joined {channel}.")
|
self.send_message(requester, f"{requester} > Joined {channel}.")
|
||||||
@@ -511,16 +522,16 @@ class DuckHuntBot:
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# Startup/rejoin joins shouldn't change config here.
|
# Startup/rejoin joins shouldn't change config here.
|
||||||
self.pending_joins.pop(channel, None)
|
self.pending_joins.pop(channel_key, None)
|
||||||
|
|
||||||
# Cancel any pending rejoin attempts for this channel
|
# Cancel any pending rejoin attempts for this channel
|
||||||
if channel in self.rejoin_tasks:
|
if channel_key in self.rejoin_tasks:
|
||||||
self.rejoin_tasks[channel].cancel()
|
self.rejoin_tasks[channel_key].cancel()
|
||||||
del self.rejoin_tasks[channel]
|
del self.rejoin_tasks[channel_key]
|
||||||
|
|
||||||
# Reset rejoin attempts counter
|
# Reset rejoin attempts counter
|
||||||
if channel in self.rejoin_attempts:
|
if channel_key in self.rejoin_attempts:
|
||||||
self.rejoin_attempts[channel] = 0
|
self.rejoin_attempts[channel_key] = 0
|
||||||
|
|
||||||
elif command == "PRIVMSG":
|
elif command == "PRIVMSG":
|
||||||
if len(params) >= 1:
|
if len(params) >= 1:
|
||||||
@@ -541,11 +552,12 @@ class DuckHuntBot:
|
|||||||
self.logger.warning(f"Kicked from {channel} by {kicker}: {reason}")
|
self.logger.warning(f"Kicked from {channel} by {kicker}: {reason}")
|
||||||
|
|
||||||
# Remove from joined channels
|
# Remove from joined channels
|
||||||
self.channels_joined.discard(channel)
|
channel_key = self._channel_key(channel)
|
||||||
|
self.channels_joined.discard(channel_key)
|
||||||
|
|
||||||
# Schedule rejoin if auto-rejoin is enabled
|
# Schedule rejoin if auto-rejoin is enabled
|
||||||
if self.get_config('connection.auto_rejoin.enabled', True):
|
if self.get_config('connection.auto_rejoin.enabled', True):
|
||||||
asyncio.create_task(self.schedule_rejoin(channel))
|
asyncio.create_task(self.schedule_rejoin(channel_key))
|
||||||
|
|
||||||
elif command == "PING":
|
elif command == "PING":
|
||||||
try:
|
try:
|
||||||
@@ -571,6 +583,10 @@ class DuckHuntBot:
|
|||||||
safe_message = sanitize_user_input(message, max_length=500)
|
safe_message = sanitize_user_input(message, max_length=500)
|
||||||
safe_user = sanitize_user_input(user, max_length=200)
|
safe_user = sanitize_user_input(user, max_length=200)
|
||||||
safe_channel = sanitize_user_input(channel, max_length=100)
|
safe_channel = sanitize_user_input(channel, max_length=100)
|
||||||
|
|
||||||
|
# Normalize channel casing for internal consistency.
|
||||||
|
if isinstance(safe_channel, str) and (safe_channel.startswith('#') or safe_channel.startswith('&')):
|
||||||
|
safe_channel = self._channel_key(safe_channel)
|
||||||
|
|
||||||
if not safe_message.startswith('!'):
|
if not safe_message.startswith('!'):
|
||||||
return
|
return
|
||||||
@@ -1221,7 +1237,9 @@ class DuckHuntBot:
|
|||||||
self.send_message(channel, f"{nick} > Invalid channel. Usage: !join <#channel>")
|
self.send_message(channel, f"{nick} > Invalid channel. Usage: !join <#channel>")
|
||||||
return
|
return
|
||||||
|
|
||||||
if target_channel in self.channels_joined:
|
target_key = self._channel_key(target_channel)
|
||||||
|
|
||||||
|
if target_key in self.channels_joined:
|
||||||
self.send_message(channel, f"{nick} > I'm already in {target_channel}.")
|
self.send_message(channel, f"{nick} > I'm already in {target_channel}.")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1230,7 +1248,7 @@ class DuckHuntBot:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Wait for server JOIN confirmation before marking joined/persisting.
|
# Wait for server JOIN confirmation before marking joined/persisting.
|
||||||
self.pending_joins[target_channel] = nick
|
self.pending_joins[target_key] = nick
|
||||||
self.send_message(channel, f"{nick} > Attempting to join {target_channel}...")
|
self.send_message(channel, f"{nick} > Attempting to join {target_channel}...")
|
||||||
|
|
||||||
async def handle_leave_channel(self, nick, channel, args):
|
async def handle_leave_channel(self, nick, channel, args):
|
||||||
@@ -1244,23 +1262,24 @@ class DuckHuntBot:
|
|||||||
self.send_message(channel, f"{nick} > Invalid channel. Usage: !leave <#channel>")
|
self.send_message(channel, f"{nick} > Invalid channel. Usage: !leave <#channel>")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
target_key = self._channel_key(target_channel)
|
||||||
|
|
||||||
# Cancel any pending rejoin attempts and forget state.
|
# Cancel any pending rejoin attempts and forget state.
|
||||||
if target_channel in self.rejoin_tasks:
|
if target_key in self.rejoin_tasks:
|
||||||
try:
|
try:
|
||||||
self.rejoin_tasks[target_channel].cancel()
|
self.rejoin_tasks[target_key].cancel()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
del self.rejoin_tasks[target_channel]
|
del self.rejoin_tasks[target_key]
|
||||||
if target_channel in self.rejoin_attempts:
|
if target_key in self.rejoin_attempts:
|
||||||
del self.rejoin_attempts[target_channel]
|
del self.rejoin_attempts[target_key]
|
||||||
|
|
||||||
self.channels_joined.discard(target_channel)
|
self.channels_joined.discard(target_key)
|
||||||
|
|
||||||
# Update in-memory config so reconnects do not rejoin the channel.
|
# Update in-memory config so reconnects do not rejoin the channel.
|
||||||
channels = self._config_channels_list()
|
channels = self._config_channels_list()
|
||||||
try:
|
try:
|
||||||
while target_channel in channels:
|
channels[:] = [c for c in channels if not (isinstance(c, str) and self._channel_key(c) == target_key)]
|
||||||
channels.remove(target_channel)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1679,9 +1698,11 @@ class DuckHuntBot:
|
|||||||
duck_type_arg = args[1] if len(args) > 1 else "normal"
|
duck_type_arg = args[1] if len(args) > 1 else "normal"
|
||||||
else:
|
else:
|
||||||
duck_type_arg = args[0] if args else "normal"
|
duck_type_arg = args[0] if args else "normal"
|
||||||
|
|
||||||
|
target_key = self._channel_key(target_channel)
|
||||||
|
|
||||||
# Validate target channel
|
# Validate target channel
|
||||||
if target_channel not in self.channels_joined:
|
if target_key not in self.channels_joined:
|
||||||
if is_private_msg:
|
if is_private_msg:
|
||||||
self.send_message(channel, f"{nick} > Channel {target_channel} is not available for duckhunt")
|
self.send_message(channel, f"{nick} > Channel {target_channel} is not available for duckhunt")
|
||||||
else:
|
else:
|
||||||
@@ -1705,7 +1726,7 @@ class DuckHuntBot:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Force spawn the specified duck type (supports multi-spawn types like couple/family)
|
# Force spawn the specified duck type (supports multi-spawn types like couple/family)
|
||||||
await self.game.force_spawn_duck(target_channel, duck_type_arg)
|
await self.game.force_spawn_duck(target_key, duck_type_arg)
|
||||||
|
|
||||||
# Send confirmation to admin (either in channel or private message)
|
# Send confirmation to admin (either in channel or private message)
|
||||||
if is_private_msg:
|
if is_private_msg:
|
||||||
|
|||||||
Reference in New Issue
Block a user