Add automatic channel rejoin functionality

- Added auto_rejoin configuration to connection settings
- Handles KICK events and automatically schedules rejoin attempts
- Configurable retry interval and max attempts
- Tracks rejoin attempts per channel with exponential backoff
- Handles JOIN confirmations to stop rejoin loops
- Proper cleanup of rejoin tasks on shutdown
- Respects shutdown and connection state before rejoining
- Logs all rejoin attempts and results for debugging
This commit is contained in:
2025-10-05 19:34:49 +01:00
parent 0176284012
commit b5613f20dd
2 changed files with 128 additions and 1 deletions

View File

@@ -10,7 +10,12 @@
"password": "duckyhunt789", "password": "duckyhunt789",
"max_retries": 3, "max_retries": 3,
"retry_delay": 5, "retry_delay": 5,
"timeout": 30 "timeout": 30,
"auto_rejoin": {
"enabled": true,
"retry_interval": 30,
"max_rejoin_attempts": 10
}
}, },
"sasl": { "sasl": {
"enabled": false, "enabled": false,

View File

@@ -24,6 +24,8 @@ class DuckHuntBot:
self.registered = False self.registered = False
self.channels_joined = set() self.channels_joined = set()
self.shutdown_requested = False self.shutdown_requested = False
self.rejoin_attempts = {} # Track rejoin attempts per channel
self.rejoin_tasks = {} # Track active rejoin tasks
self.logger.info("🤖 Initializing DuckHunt Bot components...") self.logger.info("🤖 Initializing DuckHunt Bot components...")
@@ -268,6 +270,79 @@ class DuckHuntBot:
self.logger.error(f"Unexpected error while sending message: {e}") self.logger.error(f"Unexpected error while sending message: {e}")
return False return False
async def schedule_rejoin(self, channel):
"""Schedule automatic rejoin attempts for a channel after being kicked"""
try:
# Cancel any existing rejoin task for this channel
if channel in self.rejoin_tasks:
self.rejoin_tasks[channel].cancel()
# Initialize rejoin attempt counter
if channel not in self.rejoin_attempts:
self.rejoin_attempts[channel] = 0
max_attempts = self.get_config('connection.auto_rejoin.max_rejoin_attempts', 10) or 10
retry_interval = self.get_config('connection.auto_rejoin.retry_interval', 30) or 30
self.logger.info(f"Scheduling rejoin for {channel} in {retry_interval} seconds")
# Create and store the rejoin task
self.rejoin_tasks[channel] = asyncio.create_task(
self._rejoin_channel_loop(channel, max_attempts, retry_interval)
)
except Exception as e:
self.logger.error(f"Error scheduling rejoin for {channel}: {e}")
async def _rejoin_channel_loop(self, channel, max_attempts, retry_interval):
"""Internal loop for attempting to rejoin a channel"""
try:
while (self.rejoin_attempts[channel] < max_attempts and
not self.shutdown_requested and
channel not in self.channels_joined):
self.rejoin_attempts[channel] += 1
self.logger.info(f"Rejoin attempt {self.rejoin_attempts[channel]}/{max_attempts} for {channel}")
# Wait before attempting rejoin
await asyncio.sleep(retry_interval)
# Check if we're still connected and registered
if not self.registered or not self.writer or self.writer.is_closing():
self.logger.warning(f"Cannot rejoin {channel}: not connected to server")
continue
# Attempt to rejoin
if self.send_raw(f"JOIN {channel}"):
self.channels_joined.add(channel)
self.logger.info(f"Successfully rejoined {channel}")
# Reset attempt counter and remove task
self.rejoin_attempts[channel] = 0
if channel in self.rejoin_tasks:
del self.rejoin_tasks[channel]
return
else:
self.logger.warning(f"Failed to send JOIN command for {channel}")
# If we've exceeded max attempts or channel was successfully joined
if self.rejoin_attempts[channel] >= max_attempts:
self.logger.error(f"Exhausted all {max_attempts} rejoin attempts for {channel}")
# Clean up
if channel in self.rejoin_tasks:
del self.rejoin_tasks[channel]
except asyncio.CancelledError:
self.logger.debug(f"Rejoin task for {channel} was cancelled")
except Exception as e:
self.logger.error(f"Error in rejoin loop for {channel}: {e}")
finally:
# Ensure cleanup
if channel in self.rejoin_tasks:
del self.rejoin_tasks[channel]
def send_message(self, target, msg): def send_message(self, target, msg):
"""Send message to target (channel or user) with enhanced error handling""" """Send message to target (channel or user) with enhanced error handling"""
if not isinstance(target, str) or not isinstance(msg, str): if not isinstance(target, str) or not isinstance(msg, str):
@@ -390,12 +465,51 @@ class DuckHuntBot:
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}")
elif command == "JOIN":
if len(params) >= 1 and prefix:
channel = params[0]
joiner_nick = prefix.split('!')[0] if '!' in prefix else prefix
our_nick = self.get_config('connection.nick', 'DuckHunt') or 'DuckHunt'
# Check if we successfully joined (or rejoined) a channel
if joiner_nick and joiner_nick.lower() == our_nick.lower():
self.channels_joined.add(channel)
self.logger.info(f"Successfully joined channel {channel}")
# Cancel any pending rejoin attempts for this channel
if channel in self.rejoin_tasks:
self.rejoin_tasks[channel].cancel()
del self.rejoin_tasks[channel]
# Reset rejoin attempts counter
if channel in self.rejoin_attempts:
self.rejoin_attempts[channel] = 0
elif command == "PRIVMSG": elif command == "PRIVMSG":
if len(params) >= 1: if len(params) >= 1:
target = params[0] target = params[0]
message = trailing or "" message = trailing or ""
await self.handle_command(prefix, target, message) await self.handle_command(prefix, target, message)
elif command == "KICK":
if len(params) >= 2:
channel = params[0]
kicked_nick = params[1]
kicker = prefix.split('!')[0] if prefix and '!' in prefix else prefix
reason = trailing or "No reason given"
# Check if we were the one kicked
our_nick = self.get_config('connection.nick', 'DuckHunt') or 'DuckHunt'
if kicked_nick and kicked_nick.lower() == our_nick.lower():
self.logger.warning(f"Kicked from {channel} by {kicker}: {reason}")
# Remove from joined channels
self.channels_joined.discard(channel)
# Schedule rejoin if auto-rejoin is enabled
if self.get_config('connection.auto_rejoin.enabled', True):
asyncio.create_task(self.schedule_rejoin(channel))
elif command == "PING": elif command == "PING":
try: try:
self.send_raw(f"PONG :{trailing}") self.send_raw(f"PONG :{trailing}")
@@ -1436,6 +1550,14 @@ class DuckHuntBot:
finally: finally:
# Fast cleanup - cancel tasks immediately with short timeout # Fast cleanup - cancel tasks immediately with short timeout
tasks_to_cancel = [task for task in [game_task, message_task] if task and not task.done()] tasks_to_cancel = [task for task in [game_task, message_task] if task and not task.done()]
# Cancel all rejoin tasks
for channel, task in list(self.rejoin_tasks.items()):
if task and not task.done():
task.cancel()
tasks_to_cancel.append(task)
self.logger.debug(f"Cancelled rejoin task for {channel}")
for task in tasks_to_cancel: for task in tasks_to_cancel:
task.cancel() task.cancel()