Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df4888bc9a | |||
| 0dd77a73df | |||
| 39fd70cba5 | |||
| bf4550ef53 | |||
| dd2177bd12 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -30,6 +30,7 @@ env/
|
|||||||
.config/
|
.config/
|
||||||
config*.yaml
|
config*.yaml
|
||||||
!config.yaml.example
|
!config.yaml.example
|
||||||
|
.archive/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
39
CHANGELOG.md
39
CHANGELOG.md
@ -7,13 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [1.2.1] - 2024-07-10
|
## [1.2.2] - 2025-02-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Improved stream reconnection verification with log entries
|
||||||
|
- Enhanced metadata endpoint discovery with multiple URL patterns
|
||||||
|
- Expanded error handling and logging for metadata fetching
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Refactored stream monitoring for better stability
|
||||||
|
- Updated metadata parsing to handle varied source formats
|
||||||
|
- General project cleanup and housekeeping
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Removed redundant "(admin only)" text from help command output when admin commands are already grouped under the Admin section
|
- Stream reconnection edge cases and timeout handling
|
||||||
- Improved help command to only show "(admin only)" when a specific admin command is queried
|
- JSON metadata parsing reliability
|
||||||
|
- Various validations
|
||||||
|
|
||||||
## [1.2.0] - 2024-07-01
|
|
||||||
|
## [1.2.1] - 2025-02-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Improved stream reconnection logic with automatic retry
|
||||||
|
- Enhanced metadata fetching with better error handling
|
||||||
|
- Expanded logging for better troubleshooting
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Issue with metadata parsing for certain stream formats
|
||||||
|
- Stream monitoring stability improvements
|
||||||
|
- Connection timeout handling
|
||||||
|
|
||||||
|
## [1.2.0] - 2025-02-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- New admin commands:
|
- New admin commands:
|
||||||
@ -32,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Improved the BotManager class to support the new terminal commands
|
- Improved the BotManager class to support the new terminal commands
|
||||||
- Updated configuration example to include the new quiet_on_start option
|
- Updated configuration example to include the new quiet_on_start option
|
||||||
|
|
||||||
## [1.0.1] - 2024-02-24
|
## [1.0.1] - 2025-02-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Configurable logging levels via config.yaml
|
- Configurable logging levels via config.yaml
|
||||||
@ -70,7 +94,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Other
|
### Other
|
||||||
- Added version information
|
- Added version information
|
||||||
|
|
||||||
## [1.0.0] - 2024-02-23
|
## [1.0.0] - 2025-02-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Initial release
|
- Initial release
|
||||||
@ -81,7 +105,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Multi-bot support
|
- Multi-bot support
|
||||||
- Configuration via YAML files
|
- Configuration via YAML files
|
||||||
|
|
||||||
[Unreleased]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.2.1...HEAD
|
[Unreleased]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.2.2...HEAD
|
||||||
|
[1.2.2]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.2.1...v1.2.2
|
||||||
[1.2.1]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.2.0...v1.2.1
|
[1.2.1]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.2.0...v1.2.1
|
||||||
[1.2.0]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.0.1...v1.2.0
|
[1.2.0]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.0.1...v1.2.0
|
||||||
[1.0.1]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.0.0...v1.0.1
|
[1.0.1]: https://code.cottongin.xyz/cottongin/Icecast-metadata-IRC-announcer/compare/v1.0.0...v1.0.1
|
||||||
|
|||||||
38
TODO.md
38
TODO.md
@ -1,16 +1,26 @@
|
|||||||
## TODO
|
## TODO 🚧
|
||||||
|
|
||||||
- Better, more clear, logging
|
- ⭐️ Better, more clear, logging
|
||||||
- Add a help command
|
- Notification system for stream admin (and bot admin)
|
||||||
- Add commands to control the stream connection from IRC
|
- Send notification if unexpected change in metadata
|
||||||
- Also to start/stop the service entirely
|
- Loss of network/stream
|
||||||
- Add a version command
|
- Live notifications (alt. for PodPing ish)
|
||||||
- Add a list of commands to the README
|
- Implement `quiet mode`
|
||||||
|
- ⭐️ Add whitelist feature
|
||||||
|
- Add commands:
|
||||||
|
- To check the status of the bot
|
||||||
|
- To check the status of the stream
|
||||||
|
- To check the status of the IRC connection
|
||||||
|
- Version command
|
||||||
- Enable joining multiple channels with a single bot instance
|
- Enable joining multiple channels with a single bot instance
|
||||||
- Add a command to force the bot to reconnect to the stream
|
|
||||||
- Add a command to check the status of the bot
|
## Complete! 🎉
|
||||||
- Add a command to check the status of the stream
|
|
||||||
- Add a command to check the status of the IRC connection
|
- ✅ Move this to a TODO.md file 😊
|
||||||
- Add a command to check the status of the bot
|
- 🟢 Added commands:
|
||||||
- Add a command to check the status of the stream
|
- ✅ Add `quiet mode` relevant commands
|
||||||
- Move this to a TODO.md file :)
|
- ✅ Add a help command
|
||||||
|
- ✅ To control the stream connection from IRC
|
||||||
|
- ✅ To start/stop the service entirely
|
||||||
|
- ✅ To quiet/unquiet the metadata announcing
|
||||||
|
- ✅ Add a list of commands to the README
|
||||||
24
bot.sh
24
bot.sh
@ -1,24 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Check if any arguments were provided
|
|
||||||
if [ $# -eq 0 ]; then
|
|
||||||
echo "Usage: $0 <config.yaml> [additional configs...]"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
# Remove any existing restart flags before starting
|
|
||||||
rm -f .restart_flag_*
|
|
||||||
|
|
||||||
python main.py "$@"
|
|
||||||
|
|
||||||
# Check for any restart flags
|
|
||||||
if ls .restart_flag_* 1> /dev/null 2>&1; then
|
|
||||||
echo "Restart flag(s) found, restarting bot(s)..."
|
|
||||||
sleep 1
|
|
||||||
continue
|
|
||||||
else
|
|
||||||
echo "Bot(s) exited without restart flag, stopping..."
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
12
launch.py
12
launch.py
@ -1,12 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import asyncio
|
|
||||||
from main import run_multiple_bots
|
|
||||||
|
|
||||||
CONFIG_FILES = [
|
|
||||||
'config_station1.yaml',
|
|
||||||
'config_station2.yaml',
|
|
||||||
'config_station3.yaml'
|
|
||||||
]
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(run_multiple_bots(CONFIG_FILES))
|
|
||||||
213
main.py
213
main.py
@ -11,7 +11,6 @@ import argparse
|
|||||||
import yaml
|
import yaml
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import sys
|
|
||||||
import inspect
|
import inspect
|
||||||
import socket
|
import socket
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -59,17 +58,6 @@ def silent_client_init(self, *args, bot_name: str = None, config: dict = None, *
|
|||||||
original_init = Client.__init__
|
original_init = Client.__init__
|
||||||
Client.__init__ = silent_client_init
|
Client.__init__ = silent_client_init
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import time
|
|
||||||
import argparse
|
|
||||||
import yaml
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Optional
|
|
||||||
import sys
|
|
||||||
import inspect
|
|
||||||
import socket
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
class RestartManager:
|
class RestartManager:
|
||||||
"""Manages restart requests for the bot.
|
"""Manages restart requests for the bot.
|
||||||
@ -443,14 +431,26 @@ class IcecastBot:
|
|||||||
# Check if recipient is a Channel object
|
# Check if recipient is a Channel object
|
||||||
is_channel = hasattr(recipient, 'name') and recipient.name.startswith('#')
|
is_channel = hasattr(recipient, 'name') and recipient.name.startswith('#')
|
||||||
|
|
||||||
if not self.allow_private_commands and not is_channel:
|
# Always allow np command in private messages
|
||||||
return
|
# Only check allow_private_commands for other commands
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if is_channel:
|
# For private messages, we need to ensure we have a valid recipient
|
||||||
|
if not is_channel:
|
||||||
|
# In private messages, send the response to the sender
|
||||||
|
self.logger.debug(f"Sending now playing info to user {message.sender}")
|
||||||
|
await message.sender.message(self.reply.format(song=self.current_song))
|
||||||
|
else:
|
||||||
|
# In channels, send to the channel
|
||||||
|
self.logger.debug(f"Sending now playing info to channel {recipient}")
|
||||||
await recipient.message(self.reply.format(song=self.current_song))
|
await recipient.message(self.reply.format(song=self.current_song))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
self.logger.error(f"Error sending now playing info: {str(e)}")
|
||||||
|
try:
|
||||||
|
# Try to send an error message back to the user
|
||||||
|
await message.sender.message(f"Error sending now playing info: {str(e)}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.command_handlers['now_playing'] = now_playing
|
self.command_handlers['now_playing'] = now_playing
|
||||||
|
|
||||||
@self.bot.on_message(create_command_pattern('help'))
|
@self.bot.on_message(create_command_pattern('help'))
|
||||||
@ -468,8 +468,8 @@ class IcecastBot:
|
|||||||
# Check if recipient is a Channel object
|
# Check if recipient is a Channel object
|
||||||
is_channel = hasattr(recipient, 'name') and recipient.name.startswith('#')
|
is_channel = hasattr(recipient, 'name') and recipient.name.startswith('#')
|
||||||
|
|
||||||
if not self.allow_private_commands and not is_channel:
|
# Always allow help command in private messages
|
||||||
return
|
# Only check allow_private_commands for non-help commands
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Parse message to check if a specific command was requested
|
# Parse message to check if a specific command was requested
|
||||||
@ -488,22 +488,23 @@ class IcecastBot:
|
|||||||
if method_name:
|
if method_name:
|
||||||
handler = self.command_handlers.get(method_name)
|
handler = self.command_handlers.get(method_name)
|
||||||
if handler and handler.__doc__:
|
if handler and handler.__doc__:
|
||||||
# Get the first line of the docstring
|
# Check if command is hidden and we're in a channel
|
||||||
first_line = handler.__doc__.strip().split('\n')[0]
|
if is_channel and hasattr(handler, 'is_hidden_command') and handler.is_hidden_command:
|
||||||
# Extract the description part after the colon
|
help_text = f"Unknown command: {pattern}"
|
||||||
desc = first_line.split(':', 1)[1].strip()
|
else:
|
||||||
# Add (admin only) for specific command queries
|
# Get the first line of the docstring
|
||||||
if pattern in self.admin_commands:
|
first_line = handler.__doc__.strip().split('\n')[0]
|
||||||
desc = f"{desc} (admin only)"
|
# Format it using the template and add (admin only) if needed
|
||||||
help_text = self.help_specific_format.format(
|
desc = first_line.split(':', 1)[1].strip()
|
||||||
prefix=self.cmd_prefix,
|
help_text = self.help_specific_format.format(
|
||||||
cmd=pattern,
|
prefix=self.cmd_prefix,
|
||||||
desc=desc
|
cmd=pattern,
|
||||||
)
|
desc=desc
|
||||||
|
)
|
||||||
|
|
||||||
# Check if user has permission for this command
|
# Check if user has permission for this command
|
||||||
if pattern in self.admin_commands and not self.is_admin(message.sender):
|
if pattern in self.admin_commands and not self.is_admin(message.sender):
|
||||||
help_text = "You don't have permission to use this command."
|
help_text = "You don't have permission to use this command."
|
||||||
else:
|
else:
|
||||||
help_text = f"No help available for command: {pattern}"
|
help_text = f"No help available for command: {pattern}"
|
||||||
else:
|
else:
|
||||||
@ -518,6 +519,10 @@ class IcecastBot:
|
|||||||
if pattern not in self.admin_commands: # If not an admin command
|
if pattern not in self.admin_commands: # If not an admin command
|
||||||
handler = self.command_handlers.get(method_name)
|
handler = self.command_handlers.get(method_name)
|
||||||
if handler and handler.__doc__:
|
if handler and handler.__doc__:
|
||||||
|
# Skip hidden commands in channel help
|
||||||
|
if is_channel and hasattr(handler, 'is_hidden_command') and handler.is_hidden_command:
|
||||||
|
continue
|
||||||
|
|
||||||
first_line = handler.__doc__.strip().split('\n')[0]
|
first_line = handler.__doc__.strip().split('\n')[0]
|
||||||
desc = first_line.split(':', 1)[1].strip()
|
desc = first_line.split(':', 1)[1].strip()
|
||||||
general_commands.append(
|
general_commands.append(
|
||||||
@ -537,6 +542,10 @@ class IcecastBot:
|
|||||||
if method_name:
|
if method_name:
|
||||||
handler = self.command_handlers.get(method_name)
|
handler = self.command_handlers.get(method_name)
|
||||||
if handler and handler.__doc__:
|
if handler and handler.__doc__:
|
||||||
|
# Skip hidden commands in channel help
|
||||||
|
if is_channel and hasattr(handler, 'is_hidden_command') and handler.is_hidden_command:
|
||||||
|
continue
|
||||||
|
|
||||||
first_line = handler.__doc__.strip().split('\n')[0]
|
first_line = handler.__doc__.strip().split('\n')[0]
|
||||||
desc = first_line.split(':', 1)[1].strip()
|
desc = first_line.split(':', 1)[1].strip()
|
||||||
# Don't add (admin only) in the list view
|
# Don't add (admin only) in the list view
|
||||||
@ -551,10 +560,22 @@ class IcecastBot:
|
|||||||
|
|
||||||
help_text = f"\x02Icecast Bot v{self.VERSION}\x02 | " + " | ".join(formatted_groups)
|
help_text = f"\x02Icecast Bot v{self.VERSION}\x02 | " + " | ".join(formatted_groups)
|
||||||
|
|
||||||
if is_channel:
|
# Send the help text to the appropriate recipient
|
||||||
|
if not is_channel:
|
||||||
|
# In private messages, send the response to the sender
|
||||||
|
self.logger.debug(f"Sending help info to user {message.sender}")
|
||||||
|
await message.sender.message(help_text)
|
||||||
|
else:
|
||||||
|
# In channels, send to the channel
|
||||||
|
self.logger.debug(f"Sending help info to channel {recipient}")
|
||||||
await recipient.message(help_text)
|
await recipient.message(help_text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
self.logger.error(f"Error sending help info: {str(e)}")
|
||||||
|
try:
|
||||||
|
# Try to send an error message back to the user
|
||||||
|
await message.sender.message(f"Error sending help info: {str(e)}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.command_handlers['help_command'] = help_command
|
self.command_handlers['help_command'] = help_command
|
||||||
|
|
||||||
@self.bot.on_message(create_command_pattern('restart'))
|
@self.bot.on_message(create_command_pattern('restart'))
|
||||||
@ -666,20 +687,59 @@ class IcecastBot:
|
|||||||
@self.bot.on_message(create_command_pattern('unquiet'))
|
@self.bot.on_message(create_command_pattern('unquiet'))
|
||||||
@self.admin_required
|
@self.admin_required
|
||||||
async def unquiet_bot(message):
|
async def unquiet_bot(message):
|
||||||
"""!unquiet: Enable song announcements
|
"""!unquiet: Re-enable song announcements
|
||||||
|
|
||||||
Resumes announcing songs in the channel.
|
The opposite of !quiet - allows the bot to resume announcing songs.
|
||||||
The bot must already be monitoring the stream.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
message: IRC message object
|
message: IRC message object
|
||||||
"""
|
"""
|
||||||
self.should_announce = True
|
try:
|
||||||
self.logger.info("Song announcements enabled by admin command")
|
self.should_announce = True
|
||||||
if hasattr(message.recipient, 'name') and message.recipient.name.startswith('#'):
|
await message.recipient.message("Song announcements re-enabled.")
|
||||||
await message.recipient.message("Song announcements enabled. Bot will now announce songs.")
|
except Exception as e:
|
||||||
|
pass
|
||||||
self.command_handlers['unquiet_bot'] = unquiet_bot
|
self.command_handlers['unquiet_bot'] = unquiet_bot
|
||||||
|
|
||||||
|
@self.bot.on_message(create_command_pattern('announce'))
|
||||||
|
@self.hidden_command
|
||||||
|
@self.admin_required
|
||||||
|
async def announce_message(message):
|
||||||
|
"""!announce: Send a message from the bot to all channels
|
||||||
|
|
||||||
|
Allows admins to use the bot to send a message to all channels.
|
||||||
|
Can be used in private message or in a channel.
|
||||||
|
|
||||||
|
Usage: !announce <message>
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: IRC message object
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Extract the message to announce (everything after the command)
|
||||||
|
match = re.match(r"^.*?announce\s+(.*?)$", message.text)
|
||||||
|
if match and match.group(1).strip():
|
||||||
|
announcement = match.group(1).strip()
|
||||||
|
|
||||||
|
# Always send to the joined channel, regardless of where the command was received
|
||||||
|
if self.channel and hasattr(self.channel, 'name') and self.channel.name.startswith('#'):
|
||||||
|
await self.channel.message(announcement)
|
||||||
|
else:
|
||||||
|
# If we somehow don't have a valid channel, inform the admin
|
||||||
|
self.logger.warning("No valid channel to announce to")
|
||||||
|
# Only send this response if in a private message
|
||||||
|
if hasattr(message.recipient, 'name') and not message.recipient.name.startswith('#'):
|
||||||
|
await message.recipient.message("No valid channel to announce to.")
|
||||||
|
else:
|
||||||
|
await message.recipient.message("Usage: !announce <message>")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error in announce_message: {str(e)}")
|
||||||
|
try:
|
||||||
|
await message.recipient.message(f"Error processing announcement: {str(e)}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.command_handlers['announce_message'] = announce_message
|
||||||
|
|
||||||
def _build_command_mappings(self):
|
def _build_command_mappings(self):
|
||||||
"""Build bidirectional mappings between command patterns and method names.
|
"""Build bidirectional mappings between command patterns and method names.
|
||||||
|
|
||||||
@ -1202,7 +1262,7 @@ class IcecastBot:
|
|||||||
if handler and handler.__doc__:
|
if handler and handler.__doc__:
|
||||||
# Extract the first line of the docstring
|
# Extract the first line of the docstring
|
||||||
first_line = handler.__doc__.strip().split('\n')[0]
|
first_line = handler.__doc__.strip().split('\n')[0]
|
||||||
# Extract the description part after the colon
|
# Remove the command prefix and colon
|
||||||
desc = first_line.split(':', 1)[1].strip()
|
desc = first_line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
commands.append(section_config['format'].format(
|
commands.append(section_config['format'].format(
|
||||||
@ -1225,19 +1285,44 @@ class IcecastBot:
|
|||||||
base_cmds = f"\x02{prefix}np\x02 (current song) • \x02{prefix}help\x02 (this help)"
|
base_cmds = f"\x02{prefix}np\x02 (current song) • \x02{prefix}help\x02 (this help)"
|
||||||
help_text = f"\x02Icecast Bot v{self.VERSION}\x02 | Commands: {base_cmds}"
|
help_text = f"\x02Icecast Bot v{self.VERSION}\x02 | Commands: {base_cmds}"
|
||||||
|
|
||||||
|
# Check if we're in a channel
|
||||||
|
recipient = getattr(message, 'recipient', None)
|
||||||
|
is_channel = hasattr(recipient, 'name') and recipient.name.startswith('#')
|
||||||
|
|
||||||
if self.is_admin(message.sender):
|
if self.is_admin(message.sender):
|
||||||
admin_cmds = (
|
admin_cmds = []
|
||||||
|
|
||||||
|
# Basic admin commands
|
||||||
|
basic_admin = (
|
||||||
f"\x02{prefix}start\x02/\x02stop\x02 (monitoring) • "
|
f"\x02{prefix}start\x02/\x02stop\x02 (monitoring) • "
|
||||||
f"\x02{prefix}reconnect\x02 (stream) • "
|
f"\x02{prefix}reconnect\x02 (stream) • "
|
||||||
f"\x02{prefix}restart\x02 (bot) • "
|
f"\x02{prefix}restart\x02 (bot) • "
|
||||||
f"\x02{prefix}quit\x02 (shutdown)"
|
f"\x02{prefix}quit\x02 (shutdown)"
|
||||||
)
|
)
|
||||||
help_text += f" | Admin: {admin_cmds}"
|
admin_cmds.append(basic_admin)
|
||||||
|
|
||||||
if hasattr(message.recipient, 'name') and message.recipient.name.startswith('#'):
|
# Add announce command only in private messages
|
||||||
await message.recipient.message(help_text)
|
if not is_channel:
|
||||||
|
admin_cmds.append(f"\x02{prefix}announce\x02 (send message)")
|
||||||
|
|
||||||
|
help_text += f" | Admin: {' • '.join(admin_cmds)}"
|
||||||
|
|
||||||
|
# Send the help text to the appropriate recipient
|
||||||
|
if not is_channel:
|
||||||
|
# In private messages, send the response to the sender
|
||||||
|
self.logger.debug(f"Sending fallback help info to user {message.sender}")
|
||||||
|
await message.sender.message(help_text)
|
||||||
|
else:
|
||||||
|
# In channels, send to the channel
|
||||||
|
self.logger.debug(f"Sending fallback help info to channel {recipient}")
|
||||||
|
await recipient.message(help_text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
self.logger.error(f"Error sending fallback help info: {str(e)}")
|
||||||
|
try:
|
||||||
|
# Try to send an error message back to the user
|
||||||
|
await message.sender.message(f"Error sending fallback help info: {str(e)}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def admin_required(self, f):
|
def admin_required(self, f):
|
||||||
"""Decorator to mark a command as requiring admin privileges.
|
"""Decorator to mark a command as requiring admin privileges.
|
||||||
@ -1255,12 +1340,16 @@ class IcecastBot:
|
|||||||
recipient = getattr(message, 'recipient', None)
|
recipient = getattr(message, 'recipient', None)
|
||||||
is_channel = hasattr(recipient, 'name') and recipient.name.startswith('#')
|
is_channel = hasattr(recipient, 'name') and recipient.name.startswith('#')
|
||||||
|
|
||||||
if not self.allow_private_commands and not is_channel:
|
# For admin commands, allow them in private messages regardless of allow_private_commands setting
|
||||||
return
|
# Just check if the user is an admin
|
||||||
|
if not is_channel: # If it's a private message
|
||||||
|
if not self.is_admin(message.sender):
|
||||||
|
return # Silently ignore if not an admin
|
||||||
|
return await f(message, *args, **kwargs) # Allow command if admin
|
||||||
|
|
||||||
|
# Channel message handling remains the same
|
||||||
if not self.is_admin(message.sender):
|
if not self.is_admin(message.sender):
|
||||||
if is_channel:
|
await recipient.message("You don't have permission to use this command.")
|
||||||
await recipient.message("You don't have permission to use this command.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
return await f(message, *args, **kwargs)
|
return await f(message, *args, **kwargs)
|
||||||
@ -1269,6 +1358,10 @@ class IcecastBot:
|
|||||||
wrapped.__doc__ = f.__doc__
|
wrapped.__doc__ = f.__doc__
|
||||||
wrapped.__name__ = f.__name__
|
wrapped.__name__ = f.__name__
|
||||||
|
|
||||||
|
# Preserve the is_hidden_command attribute if it exists
|
||||||
|
if hasattr(f, 'is_hidden_command'):
|
||||||
|
wrapped.is_hidden_command = f.is_hidden_command
|
||||||
|
|
||||||
# Add the command pattern to admin_commands set
|
# Add the command pattern to admin_commands set
|
||||||
if f.__doc__ and f.__doc__.strip().startswith('!'):
|
if f.__doc__ and f.__doc__.strip().startswith('!'):
|
||||||
pattern = f.__doc__.strip().split(':', 1)[0].strip('!')
|
pattern = f.__doc__.strip().split(':', 1)[0].strip('!')
|
||||||
@ -1276,6 +1369,22 @@ class IcecastBot:
|
|||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
def hidden_command(self, f):
|
||||||
|
"""Decorator to mark a command as hidden from help output in channels.
|
||||||
|
|
||||||
|
Hidden commands will only appear in help output when the help command
|
||||||
|
is used in a private message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
f: The command handler function to wrap.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The wrapped function with a hidden flag.
|
||||||
|
"""
|
||||||
|
# Set a flag on the function to mark it as hidden
|
||||||
|
f.is_hidden_command = True
|
||||||
|
return f
|
||||||
|
|
||||||
async def run_multiple_bots(config_paths: List[str]):
|
async def run_multiple_bots(config_paths: List[str]):
|
||||||
"""Run multiple bot instances concurrently.
|
"""Run multiple bot instances concurrently.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user