karma plugin added. various debug fixes to funguy.py
This commit is contained in:
@@ -733,6 +733,53 @@ Fetches current time information for locations using the TimeAPI.io service.
|
|||||||
!time New York
|
!time New York
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Karma Tracking System
|
||||||
|
|
||||||
|
**☯ !karma [user]**
|
||||||
|
Track karma points for users with leaderboards and statistics. Supports display names and Matrix IDs.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Give/take karma points from users using display names or Matrix IDs
|
||||||
|
- Track karma history with timestamps
|
||||||
|
- View karma leaderboards (top/bottom)
|
||||||
|
- Rate limiting to prevent spam
|
||||||
|
- Room-specific karma tracking
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `!karma <user>` - Show karma for a user
|
||||||
|
- `!karma++ <user>` - Give +1 karma to a user
|
||||||
|
- `!karma-- <user>` - Give -1 karma to a user
|
||||||
|
- `!karma top [n]` - Show top karma entries
|
||||||
|
- `!karma bottom [n]` - Show bottom karma entries
|
||||||
|
- `!karma rank <user>` - Show rank of user
|
||||||
|
- `!karma stats` - Show overall statistics
|
||||||
|
- `!karma history <user>` - Show recent karma history
|
||||||
|
|
||||||
|
**Shortcuts:**
|
||||||
|
- `!++ <user>` - Same as `!karma++ <user>`
|
||||||
|
- `!-- <user>` - Same as `!karma-- <user>`
|
||||||
|
- `<user>++` - Give +1 karma (inline)
|
||||||
|
- `<user>--` - Give -1 karma (inline)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
!karma @user:server.com
|
||||||
|
!karma++ @user:server.com
|
||||||
|
!karma top 5
|
||||||
|
!karma bottom 3
|
||||||
|
!karma rank @user:server.com
|
||||||
|
!karma stats
|
||||||
|
!karma history @user:server.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- You cannot modify your own karma
|
||||||
|
- There is a 5 second cooldown between votes
|
||||||
|
- Karma is tracked separately per room
|
||||||
|
- Display names with emojis are supported
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
- Ensure all environment variables are set correctly
|
- Ensure all environment variables are set correctly
|
||||||
- Check that required services are running (Stable Diffusion API, Ollama, etc.)
|
- Check that required services are running (Stable Diffusion API, Ollama, etc.)
|
||||||
- Verify plugin permissions and whitelist settings
|
- Verify plugin permissions and whitelist settings
|
||||||
|
|||||||
+1
-1
@@ -14,4 +14,4 @@ config_file = "funguy.conf"
|
|||||||
[plugins.disabled]
|
[plugins.disabled]
|
||||||
"!uFhErnfpYhhlauJsNK:matrix.org" = [ "youtube-preview", "ai", "proxy",]
|
"!uFhErnfpYhhlauJsNK:matrix.org" = [ "youtube-preview", "ai", "proxy",]
|
||||||
"!vYcfWXpPvxeQvhlFdV:matrix.org" = []
|
"!vYcfWXpPvxeQvhlFdV:matrix.org" = []
|
||||||
"!NXdVjDXPxXowPkrJJY:matrix.org" = [ "karma"]
|
"!NXdVjDXPxXowPkrJJY:matrix.org" = []
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Funguy Bot Class - A modular Matrix bot with plugin support
|
Funguy Bot Class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Importing necessary libraries and modules
|
# Importing necessary libraries and modules
|
||||||
@@ -13,76 +13,78 @@ from dotenv import load_dotenv # Library for loading environment variables from
|
|||||||
import time # Time-related functions
|
import time # Time-related functions
|
||||||
import sys # System-specific parameters and functions
|
import sys # System-specific parameters and functions
|
||||||
import toml # Library for parsing TOML configuration files
|
import toml # Library for parsing TOML configuration files
|
||||||
|
import socket # For network diagnostics
|
||||||
|
|
||||||
# Importing FunguyConfig class from plugins.config module
|
# Importing FunguyConfig class from plugins.config module
|
||||||
from plugins.config import FunguyConfig
|
from plugins.config import FunguyConfig
|
||||||
|
|
||||||
|
|
||||||
class FunguyBot:
|
class FunguyBot:
|
||||||
"""
|
"""
|
||||||
A bot class for managing plugins and handling commands in a Matrix chat environment.
|
A bot class for managing plugins and handling commands in a Matrix chat environment.
|
||||||
|
|
||||||
Methods:
|
|
||||||
- __init__: Constructor method for initializing the bot.
|
|
||||||
- load_dotenv: Method to load environment variables from a .env file.
|
|
||||||
- setup_logging: Method to configure logging settings.
|
|
||||||
- load_plugins: Method to load plugins from the specified directory.
|
|
||||||
- reload_plugins: Method to reload all plugins.
|
|
||||||
- load_config: Method to load configuration settings.
|
|
||||||
- load_disabled_plugins: Method to load disabled plugins from configuration file.
|
|
||||||
- save_disabled_plugins: Method to save disabled plugins to configuration file.
|
|
||||||
- handle_commands: Method to handle incoming commands and dispatch them to appropriate plugins.
|
|
||||||
- rehash_config: Method to rehash the configuration settings.
|
|
||||||
- disable_plugin: Method to disable a plugin for a specific room.
|
|
||||||
- enable_plugin: Method to enable a plugin for a specific room.
|
|
||||||
- run: Method to initialize and run the bot.
|
|
||||||
|
|
||||||
Properties:
|
|
||||||
- PLUGINS_DIR: Directory where plugins are stored
|
|
||||||
- PLUGINS: Dictionary to store loaded plugins
|
|
||||||
- config: Configuration object
|
|
||||||
- bot: Bot object
|
|
||||||
- disabled_plugins: Dictionary to store disabled plugins for each room
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""
|
"""
|
||||||
Constructor method for FunguyBot class.
|
Constructor method for FunguyBot class.
|
||||||
"""
|
"""
|
||||||
|
print("[INIT] Starting FunguyBot initialization...")
|
||||||
|
|
||||||
# Setting up instance variables
|
# Setting up instance variables
|
||||||
self.PLUGINS_DIR = "plugins" # Directory where plugins are stored
|
self.PLUGINS_DIR = "plugins" # Directory where plugins are stored
|
||||||
self.PLUGINS = {} # Dictionary to store loaded plugins
|
self.PLUGINS = {} # Dictionary to store loaded plugins
|
||||||
self.config = None # Configuration object
|
self.config = None # Configuration object
|
||||||
self.bot = None # Bot object
|
self.bot = None # Bot object
|
||||||
self.disabled_plugins = {} # Dictionary to store disabled plugins for each room
|
self.disabled_plugins = {} # Dictionary to store disabled plugins for each room
|
||||||
|
|
||||||
|
print("[INIT] Loading environment variables...")
|
||||||
self.load_dotenv() # Loading environment variables from .env file
|
self.load_dotenv() # Loading environment variables from .env file
|
||||||
|
|
||||||
|
print("[INIT] Setting up logging...")
|
||||||
self.setup_logging() # Setting up logging configurations
|
self.setup_logging() # Setting up logging configurations
|
||||||
|
|
||||||
|
print("[INIT] Loading plugins...")
|
||||||
self.load_plugins() # Loading plugins
|
self.load_plugins() # Loading plugins
|
||||||
|
|
||||||
|
print("[INIT] Loading config...")
|
||||||
self.load_config() # Loading bot configuration
|
self.load_config() # Loading bot configuration
|
||||||
|
|
||||||
|
print("[INIT] Loading disabled plugins...")
|
||||||
self.load_disabled_plugins() # Loading disabled plugins from configuration file
|
self.load_disabled_plugins() # Loading disabled plugins from configuration file
|
||||||
|
|
||||||
|
print("[INIT] FunguyBot initialization complete!")
|
||||||
|
|
||||||
def load_dotenv(self):
|
def load_dotenv(self):
|
||||||
"""
|
"""
|
||||||
Method to load environment variables from a .env file.
|
Method to load environment variables from a .env file.
|
||||||
"""
|
"""
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
print("[ENV] Environment variables loaded")
|
||||||
|
|
||||||
def setup_logging(self):
|
def setup_logging(self):
|
||||||
"""
|
"""
|
||||||
Method to configure logging settings.
|
Method to configure logging settings.
|
||||||
"""
|
"""
|
||||||
# Basic configuration for logging messages to console
|
# Get log level from environment, default to INFO
|
||||||
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||||
|
|
||||||
# Configure logging format
|
# Convert string to logging constant
|
||||||
|
level_map = {
|
||||||
|
"DEBUG": logging.DEBUG,
|
||||||
|
"INFO": logging.INFO,
|
||||||
|
"WARNING": logging.WARNING,
|
||||||
|
"ERROR": logging.ERROR,
|
||||||
|
"CRITICAL": logging.CRITICAL
|
||||||
|
}
|
||||||
|
level = level_map.get(log_level, logging.INFO)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
level=getattr(logging, log_level, logging.INFO)
|
level=level
|
||||||
)
|
)
|
||||||
|
logging.getLogger().setLevel(level)
|
||||||
|
|
||||||
# Set specific loggers to appropriate levels
|
# Optionally silence noisy libraries
|
||||||
logging.getLogger().setLevel(getattr(logging, log_level, logging.INFO))
|
logging.getLogger("aiohttp").setLevel(logging.WARNING)
|
||||||
logging.getLogger("simplematrixbotlib").setLevel(logging.WARNING)
|
|
||||||
logging.getLogger("nio").setLevel(logging.WARNING)
|
logging.getLogger("nio").setLevel(logging.WARNING)
|
||||||
|
|
||||||
logging.info(f"Logging configured with level: {log_level}")
|
logging.info(f"Logging configured with level: {log_level}")
|
||||||
@@ -92,20 +94,16 @@ class FunguyBot:
|
|||||||
Method to load plugins from the specified directory.
|
Method to load plugins from the specified directory.
|
||||||
"""
|
"""
|
||||||
# Iterating through files in the plugins directory
|
# Iterating through files in the plugins directory
|
||||||
plugin_count = 0
|
|
||||||
for plugin_file in os.listdir(self.PLUGINS_DIR):
|
for plugin_file in os.listdir(self.PLUGINS_DIR):
|
||||||
if plugin_file.endswith(".py") and plugin_file != "__init__.py":
|
if plugin_file.endswith(".py"): # Checking if file is a Python file
|
||||||
plugin_name = os.path.splitext(plugin_file)[0] # Extracting plugin name
|
plugin_name = os.path.splitext(plugin_file)[0] # Extracting plugin name
|
||||||
try:
|
try:
|
||||||
# Importing plugin module dynamically
|
# Importing plugin module dynamically
|
||||||
module = importlib.import_module(f"{self.PLUGINS_DIR}.{plugin_name}")
|
module = importlib.import_module(f"{self.PLUGINS_DIR}.{plugin_name}")
|
||||||
self.PLUGINS[plugin_name] = module # Storing loaded plugin module
|
self.PLUGINS[plugin_name] = module # Storing loaded plugin module
|
||||||
logging.info(f"Loaded plugin: {plugin_name}")
|
logging.info(f"Loaded plugin: {plugin_name}") # Logging successful plugin loading
|
||||||
plugin_count += 1
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading plugin {plugin_name}: {e}", exc_info=True)
|
logging.error(f"Error loading plugin {plugin_name}: {e}") # Logging error if plugin loading fails
|
||||||
|
|
||||||
logging.info(f"Total plugins loaded: {plugin_count}")
|
|
||||||
|
|
||||||
def setup_plugins(self):
|
def setup_plugins(self):
|
||||||
"""
|
"""
|
||||||
@@ -115,45 +113,28 @@ class FunguyBot:
|
|||||||
that plugins which register custom event listeners (e.g. on_custom_event
|
that plugins which register custom event listeners (e.g. on_custom_event
|
||||||
for RoomMemberEvent) receive a valid bot instance.
|
for RoomMemberEvent) receive a valid bot instance.
|
||||||
"""
|
"""
|
||||||
setup_count = 0
|
|
||||||
for plugin_name, plugin_module in self.PLUGINS.items():
|
for plugin_name, plugin_module in self.PLUGINS.items():
|
||||||
if hasattr(plugin_module, "setup") and callable(plugin_module.setup):
|
if hasattr(plugin_module, "setup") and callable(plugin_module.setup):
|
||||||
try:
|
try:
|
||||||
plugin_module.setup(self.bot)
|
plugin_module.setup(self.bot)
|
||||||
logging.info(f"Setup called for plugin: {plugin_name}")
|
logging.info(f"Setup called for plugin: {plugin_name}")
|
||||||
setup_count += 1
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error during setup of plugin {plugin_name}: {e}", exc_info=True)
|
logging.error(f"Error during setup of plugin {plugin_name}: {e}")
|
||||||
|
|
||||||
logging.info(f"Setup completed for {setup_count} plugins")
|
|
||||||
|
|
||||||
def reload_plugins(self):
|
def reload_plugins(self):
|
||||||
"""
|
"""
|
||||||
Method to reload all plugins.
|
Method to reload all plugins.
|
||||||
"""
|
"""
|
||||||
logging.info("Reloading plugins...")
|
self.PLUGINS = {} # Clearing loaded plugins dictionary
|
||||||
|
|
||||||
# Clear loaded plugins dictionary
|
|
||||||
self.PLUGINS.clear()
|
|
||||||
|
|
||||||
# Unloading modules from sys.modules
|
# Unloading modules from sys.modules
|
||||||
modules_to_remove = []
|
for plugin_name in list(sys.modules.keys()):
|
||||||
for module_name in sys.modules.keys():
|
if plugin_name.startswith(self.PLUGINS_DIR + "."):
|
||||||
if module_name.startswith(self.PLUGINS_DIR + "."):
|
del sys.modules[plugin_name] # Deleting plugin module from system modules
|
||||||
modules_to_remove.append(module_name)
|
self.load_plugins() # Reloading plugins
|
||||||
|
|
||||||
for module_name in modules_to_remove:
|
|
||||||
del sys.modules[module_name]
|
|
||||||
|
|
||||||
# Reload plugins
|
|
||||||
self.load_plugins()
|
|
||||||
|
|
||||||
# Re-run setup for any plugin that needs it (bot already exists at this point)
|
# Re-run setup for any plugin that needs it (bot already exists at this point)
|
||||||
if self.bot is not None:
|
if self.bot is not None:
|
||||||
self.setup_plugins()
|
self.setup_plugins()
|
||||||
|
|
||||||
logging.info("Plugins reloaded successfully")
|
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
"""
|
"""
|
||||||
Method to load configuration settings.
|
Method to load configuration settings.
|
||||||
@@ -167,18 +148,11 @@ class FunguyBot:
|
|||||||
"""
|
"""
|
||||||
# Checking if configuration file exists
|
# Checking if configuration file exists
|
||||||
if os.path.exists('funguy.conf'):
|
if os.path.exists('funguy.conf'):
|
||||||
try:
|
|
||||||
# Loading configuration data from TOML file
|
# Loading configuration data from TOML file
|
||||||
with open('funguy.conf', 'r') as f:
|
with open('funguy.conf', 'r') as f:
|
||||||
config_data = toml.load(f)
|
config_data = toml.load(f)
|
||||||
# Extracting disabled plugins from configuration data
|
# Extracting disabled plugins from configuration data
|
||||||
self.disabled_plugins = config_data.get('plugins', {}).get('disabled', {})
|
self.disabled_plugins = config_data.get('plugins', {}).get('disabled', {})
|
||||||
logging.info(f"Loaded disabled plugins configuration for {len(self.disabled_plugins)} rooms")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error loading disabled plugins configuration: {e}")
|
|
||||||
self.disabled_plugins = {}
|
|
||||||
else:
|
|
||||||
logging.info("No funguy.conf found, starting with empty disabled plugins list")
|
|
||||||
|
|
||||||
def save_disabled_plugins(self):
|
def save_disabled_plugins(self):
|
||||||
"""
|
"""
|
||||||
@@ -187,25 +161,14 @@ class FunguyBot:
|
|||||||
existing_config = {}
|
existing_config = {}
|
||||||
# Checking if configuration file exists
|
# Checking if configuration file exists
|
||||||
if os.path.exists('funguy.conf'):
|
if os.path.exists('funguy.conf'):
|
||||||
try:
|
|
||||||
# Loading existing configuration data
|
# Loading existing configuration data
|
||||||
with open('funguy.conf', 'r') as f:
|
with open('funguy.conf', 'r') as f:
|
||||||
existing_config = toml.load(f)
|
existing_config = toml.load(f)
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error reading funguy.conf: {e}")
|
|
||||||
|
|
||||||
# Updating configuration data with disabled plugins
|
# Updating configuration data with disabled plugins
|
||||||
if 'plugins' not in existing_config:
|
existing_config['plugins'] = {'disabled': self.disabled_plugins}
|
||||||
existing_config['plugins'] = {}
|
|
||||||
existing_config['plugins']['disabled'] = self.disabled_plugins
|
|
||||||
|
|
||||||
# Writing updated configuration data back to file
|
# Writing updated configuration data back to file
|
||||||
try:
|
|
||||||
with open('funguy.conf', 'w') as f:
|
with open('funguy.conf', 'w') as f:
|
||||||
toml.dump(existing_config, f)
|
toml.dump(existing_config, f)
|
||||||
logging.info("Saved disabled plugins configuration")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error saving disabled plugins configuration: {e}")
|
|
||||||
|
|
||||||
async def handle_commands(self, room, message):
|
async def handle_commands(self, room, message):
|
||||||
"""
|
"""
|
||||||
@@ -217,9 +180,9 @@ class FunguyBot:
|
|||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("reload"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("reload"):
|
||||||
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
||||||
self.reload_plugins() # Reloading plugins
|
self.reload_plugins() # Reloading plugins
|
||||||
await self.bot.api.send_text_message(room.room_id, "✅ Plugins reloaded successfully")
|
await self.bot.api.send_text_message(room.room_id, "Plugins reloaded successfully") # Sending success message
|
||||||
else:
|
else:
|
||||||
await self.bot.api.send_text_message(room.room_id, "❌ You are not authorized to reload plugins.")
|
await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") # Sending unauthorized message
|
||||||
return
|
return
|
||||||
|
|
||||||
# Disable plugin command
|
# Disable plugin command
|
||||||
@@ -227,13 +190,13 @@ class FunguyBot:
|
|||||||
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
||||||
args = match.args() # Getting command arguments
|
args = match.args() # Getting command arguments
|
||||||
if len(args) != 2: # Checking if correct number of arguments provided
|
if len(args) != 2: # Checking if correct number of arguments provided
|
||||||
await self.bot.api.send_text_message(room.room_id, "Usage: !disable <plugin> <room_id>")
|
await self.bot.api.send_text_message(room.room_id, "Usage: !disable <plugin> <room_id>") # Sending usage message
|
||||||
else:
|
else:
|
||||||
plugin_name, room_id = args # Extracting plugin name and room ID
|
plugin_name, room_id = args # Extracting plugin name and room ID
|
||||||
await self.disable_plugin(room_id, plugin_name) # Disabling plugin
|
await self.disable_plugin(room_id, plugin_name) # Disabling plugin
|
||||||
await self.bot.api.send_text_message(room.room_id, f"✅ Plugin '{plugin_name}' disabled for room '{room_id}'")
|
await self.bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' disabled for room '{room_id}'") # Sending success message
|
||||||
else:
|
else:
|
||||||
await self.bot.api.send_text_message(room.room_id, "❌ You are not authorized to disable plugins.")
|
await self.bot.api.send_text_message(room.room_id, "You are not authorized to disable plugins.") # Sending unauthorized message
|
||||||
return
|
return
|
||||||
|
|
||||||
# Enable plugin command
|
# Enable plugin command
|
||||||
@@ -241,85 +204,88 @@ class FunguyBot:
|
|||||||
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
||||||
args = match.args() # Getting command arguments
|
args = match.args() # Getting command arguments
|
||||||
if len(args) != 2: # Checking if correct number of arguments provided
|
if len(args) != 2: # Checking if correct number of arguments provided
|
||||||
await self.bot.api.send_text_message(room.room_id, "Usage: !enable <plugin> <room_id>")
|
await self.bot.api.send_text_message(room.room_id, "Usage: !enable <plugin> <room_id>") # Sending usage message
|
||||||
else:
|
else:
|
||||||
plugin_name, room_id = args # Extracting plugin name and room ID
|
plugin_name, room_id = args # Extracting plugin name and room ID
|
||||||
await self.enable_plugin(room_id, plugin_name) # Enabling plugin
|
await self.enable_plugin(room_id, plugin_name) # Enabling plugin
|
||||||
await self.bot.api.send_text_message(room.room_id, f"✅ Plugin '{plugin_name}' enabled for room '{room_id}'")
|
await self.bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' enabled for room '{room_id}'") # Sending success message
|
||||||
else:
|
else:
|
||||||
await self.bot.api.send_text_message(room.room_id, "❌ You are not authorized to enable plugins.")
|
await self.bot.api.send_text_message(room.room_id, "You are not authorized to enable plugins.") # Sending unauthorized message
|
||||||
|
return
|
||||||
|
|
||||||
|
# List plugins command with descriptions (bold plugin names)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("plugins"):
|
||||||
|
if self.PLUGINS:
|
||||||
|
# Build a list with plugin names and their descriptions
|
||||||
|
plugin_list = []
|
||||||
|
for plugin_name in sorted(self.PLUGINS.keys()):
|
||||||
|
plugin_module = self.PLUGINS[plugin_name]
|
||||||
|
# Try to get description from plugin
|
||||||
|
description = getattr(plugin_module, "__description__", None)
|
||||||
|
if not description:
|
||||||
|
# Try to get from docstring
|
||||||
|
docstring = getattr(plugin_module, "__doc__", None)
|
||||||
|
if docstring:
|
||||||
|
# Get first line of docstring
|
||||||
|
description = docstring.strip().split('\n')[0]
|
||||||
|
else:
|
||||||
|
description = "No description available"
|
||||||
|
|
||||||
|
# Make plugin name bold using HTML <strong> tag
|
||||||
|
plugin_list.append(f"<strong>[{plugin_name}.py]</strong>: {description}")
|
||||||
|
|
||||||
|
# Send as HTML message (bold will render)
|
||||||
|
response = "\n".join(plugin_list)
|
||||||
|
# Split into multiple messages if too long
|
||||||
|
if len(response) > 4000:
|
||||||
|
chunk = ""
|
||||||
|
for line in plugin_list:
|
||||||
|
if len(chunk) + len(line) + 1 > 4000:
|
||||||
|
await self.bot.api.send_markdown_message(room.room_id, chunk)
|
||||||
|
chunk = line + "\n"
|
||||||
|
else:
|
||||||
|
chunk += line + "\n"
|
||||||
|
if chunk:
|
||||||
|
await self.bot.api.send_markdown_message(room.room_id, chunk)
|
||||||
|
else:
|
||||||
|
await self.bot.api.send_markdown_message(room.room_id, response)
|
||||||
|
else:
|
||||||
|
await self.bot.api.send_text_message(room.room_id, "No plugins are currently loaded.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Rehash config command
|
# Rehash config command
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"):
|
||||||
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
||||||
self.rehash_config() # Rehashing configuration
|
self.rehash_config() # Rehashing configuration
|
||||||
await self.bot.api.send_text_message(room.room_id, "✅ Configuration rehashed successfully")
|
await self.bot.api.send_text_message(room.room_id, "Config rehashed") # Sending success message
|
||||||
else:
|
else:
|
||||||
await self.bot.api.send_text_message(room.room_id, "❌ You are not authorized to rehash configuration.")
|
await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") # Sending unauthorized message
|
||||||
return
|
return
|
||||||
|
|
||||||
# List plugins command
|
# Dispatching commands to plugins
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("plugins"):
|
|
||||||
if self.PLUGINS:
|
|
||||||
plugin_list = "\n".join([f" • {name}" for name in sorted(self.PLUGINS.keys())])
|
|
||||||
await self.bot.api.send_markdown_message(
|
|
||||||
room.room_id,
|
|
||||||
f"**📦 Loaded Plugins ({len(self.PLUGINS)})**\n\n{plugin_list}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await self.bot.api.send_text_message(room.room_id, "No plugins are currently loaded.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Dispatching commands to plugins (only if plugin is not disabled for this room)
|
|
||||||
for plugin_name, plugin_module in self.PLUGINS.items():
|
for plugin_name, plugin_module in self.PLUGINS.items():
|
||||||
# Check if plugin is disabled for this room
|
if plugin_name not in self.disabled_plugins.get(room.room_id, []):
|
||||||
if plugin_name in self.disabled_plugins.get(room.room_id, []):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if plugin has handle_command function
|
|
||||||
if hasattr(plugin_module, "handle_command") and callable(plugin_module.handle_command):
|
|
||||||
try:
|
try:
|
||||||
await plugin_module.handle_command(room, message, self.bot, self.config.prefix, self.config)
|
await plugin_module.handle_command(room, message, self.bot, self.config.prefix, self.config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in plugin {plugin_name}: {e}", exc_info=True)
|
logging.error(f"Error in plugin {plugin_name}: {e}", exc_info=True)
|
||||||
# Don't break the loop, continue with other plugins
|
|
||||||
|
|
||||||
def rehash_config(self):
|
def rehash_config(self):
|
||||||
"""
|
"""
|
||||||
Method to rehash the configuration settings.
|
Method to rehash the configuration settings.
|
||||||
"""
|
"""
|
||||||
logging.info("Rehashing configuration...")
|
|
||||||
try:
|
|
||||||
# Reload environment variables
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Reload configuration
|
|
||||||
del self.config # Deleting current configuration object
|
del self.config # Deleting current configuration object
|
||||||
self.config = FunguyConfig() # Creating new instance of FunguyConfig to load updated configuration
|
self.config = FunguyConfig() # Creating new instance of FunguyConfig to load updated configuration
|
||||||
|
|
||||||
# Reload disabled plugins
|
|
||||||
self.load_disabled_plugins()
|
|
||||||
|
|
||||||
logging.info("Configuration rehashed successfully")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error rehashing configuration: {e}")
|
|
||||||
|
|
||||||
async def disable_plugin(self, room_id, plugin_name):
|
async def disable_plugin(self, room_id, plugin_name):
|
||||||
"""
|
"""
|
||||||
Method to disable a plugin for a specific room.
|
Method to disable a plugin for a specific room.
|
||||||
"""
|
"""
|
||||||
if plugin_name not in self.PLUGINS:
|
|
||||||
logging.warning(f"Attempted to disable non-existent plugin: {plugin_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if room_id not in self.disabled_plugins:
|
if room_id not in self.disabled_plugins:
|
||||||
self.disabled_plugins[room_id] = [] # Creating entry for room ID if not exist
|
self.disabled_plugins[room_id] = [] # Creating entry for room ID if not exist
|
||||||
|
|
||||||
if plugin_name not in self.disabled_plugins[room_id]:
|
if plugin_name not in self.disabled_plugins[room_id]:
|
||||||
self.disabled_plugins[room_id].append(plugin_name) # Adding plugin to list of disabled plugins for the room
|
self.disabled_plugins[room_id].append(plugin_name) # Adding plugin to list of disabled plugins for the room
|
||||||
self.save_disabled_plugins() # Saving disabled plugins to configuration file
|
self.save_disabled_plugins() # Saving disabled plugins to configuration file
|
||||||
logging.info(f"Plugin '{plugin_name}' disabled for room {room_id}")
|
|
||||||
|
|
||||||
async def enable_plugin(self, room_id, plugin_name):
|
async def enable_plugin(self, room_id, plugin_name):
|
||||||
"""
|
"""
|
||||||
@@ -327,60 +293,143 @@ class FunguyBot:
|
|||||||
"""
|
"""
|
||||||
if room_id in self.disabled_plugins and plugin_name in self.disabled_plugins[room_id]:
|
if room_id in self.disabled_plugins and plugin_name in self.disabled_plugins[room_id]:
|
||||||
self.disabled_plugins[room_id].remove(plugin_name) # Removing plugin from list of disabled plugins for the room
|
self.disabled_plugins[room_id].remove(plugin_name) # Removing plugin from list of disabled plugins for the room
|
||||||
|
|
||||||
# Clean up empty room entries
|
|
||||||
if not self.disabled_plugins[room_id]:
|
|
||||||
del self.disabled_plugins[room_id]
|
|
||||||
|
|
||||||
self.save_disabled_plugins() # Saving disabled plugins to configuration file
|
self.save_disabled_plugins() # Saving disabled plugins to configuration file
|
||||||
logging.info(f"Plugin '{plugin_name}' enabled for room {room_id}")
|
|
||||||
|
def test_connectivity(self, hostname, port=443):
|
||||||
|
"""
|
||||||
|
Test network connectivity to Matrix server.
|
||||||
|
"""
|
||||||
|
logging.info(f"Testing connectivity to {hostname}:{port}...")
|
||||||
|
try:
|
||||||
|
# Test DNS resolution
|
||||||
|
ip_address = socket.gethostbyname(hostname)
|
||||||
|
logging.info(f"✓ DNS resolution successful: {hostname} -> {ip_address}")
|
||||||
|
|
||||||
|
# Test socket connection
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(10)
|
||||||
|
result = sock.connect_ex((hostname, port))
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
logging.info(f"✓ Socket connection successful to {hostname}:{port}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logging.error(f"✗ Socket connection failed to {hostname}:{port} (error code: {result})")
|
||||||
|
return False
|
||||||
|
except socket.gaierror as e:
|
||||||
|
logging.error(f"✗ DNS resolution failed for {hostname}: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"✗ Connectivity test failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
Method to initialize and run the bot.
|
Method to initialize and run the bot.
|
||||||
"""
|
"""
|
||||||
try:
|
print("\n" + "="*60)
|
||||||
|
print("FUNGUY BOT - STARTING")
|
||||||
|
print("="*60 + "\n")
|
||||||
|
|
||||||
# Retrieving Matrix credentials from environment variables
|
# Retrieving Matrix credentials from environment variables
|
||||||
MATRIX_URL = os.getenv("MATRIX_URL")
|
MATRIX_URL = os.getenv("MATRIX_URL")
|
||||||
MATRIX_USER = os.getenv("MATRIX_USER")
|
MATRIX_USER = os.getenv("MATRIX_USER")
|
||||||
MATRIX_PASS = os.getenv("MATRIX_PASS")
|
MATRIX_PASS = os.getenv("MATRIX_PASS")
|
||||||
|
|
||||||
# Validate credentials
|
# Validate credentials
|
||||||
if not all([MATRIX_URL, MATRIX_USER, MATRIX_PASS]):
|
if not MATRIX_URL:
|
||||||
logging.error("Missing Matrix credentials in .env file. Please check MATRIX_URL, MATRIX_USER, MATRIX_PASS")
|
logging.error("MATRIX_URL not set in .env file")
|
||||||
|
return
|
||||||
|
if not MATRIX_USER:
|
||||||
|
logging.error("MATRIX_USER not set in .env file")
|
||||||
|
return
|
||||||
|
if not MATRIX_PASS:
|
||||||
|
logging.error("MATRIX_PASS not set in .env file")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logging.info(f"Matrix URL: {MATRIX_URL}")
|
||||||
|
logging.info(f"Matrix User: {MATRIX_USER}")
|
||||||
|
|
||||||
|
# Extract hostname from URL for connectivity test
|
||||||
|
hostname = MATRIX_URL.replace("https://", "").replace("http://", "").split("/")[0]
|
||||||
|
|
||||||
|
# Test connectivity before attempting to connect
|
||||||
|
logging.info("="*40)
|
||||||
|
logging.info("RUNNING NETWORK DIAGNOSTICS")
|
||||||
|
logging.info("="*40)
|
||||||
|
|
||||||
|
if not self.test_connectivity(hostname, 443):
|
||||||
|
logging.error("Connectivity test failed. Please check:")
|
||||||
|
logging.error(" 1. Your internet connection")
|
||||||
|
logging.error(" 2. Firewall settings (outbound port 443)")
|
||||||
|
logging.error(" 3. DNS resolution")
|
||||||
|
logging.error(f" 4. If {hostname} is accessible")
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info("="*40)
|
||||||
|
logging.info("ATTEMPTING MATRIX CONNECTION")
|
||||||
|
logging.info("="*40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info(f"Creating credentials object for {MATRIX_USER}...")
|
||||||
creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASS) # Creating credentials object
|
creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASS) # Creating credentials object
|
||||||
|
logging.info("✓ Credentials object created")
|
||||||
|
|
||||||
|
logging.info("Creating bot instance...")
|
||||||
self.bot = botlib.Bot(creds, self.config) # Creating bot instance
|
self.bot = botlib.Bot(creds, self.config) # Creating bot instance
|
||||||
|
logging.info("✓ Bot instance created")
|
||||||
|
|
||||||
logging.info(f"Bot starting with user: {MATRIX_USER}")
|
# Check if async_client is available
|
||||||
logging.info(f"Connected to homeserver: {MATRIX_URL}")
|
if hasattr(self.bot, 'async_client'):
|
||||||
|
logging.info("✓ Async client available")
|
||||||
|
else:
|
||||||
|
logging.warning("⚠ Async client not yet available (will be created on login)")
|
||||||
|
|
||||||
|
logging.info("Calling setup_plugins()...")
|
||||||
# Call setup() on any plugin that defines it, now that self.bot exists.
|
# Call setup() on any plugin that defines it, now that self.bot exists.
|
||||||
# This is what registers custom event listeners such as the join-event
|
|
||||||
# listener in the welcome plugin.
|
|
||||||
self.setup_plugins()
|
self.setup_plugins()
|
||||||
|
logging.info("✓ Plugin setup complete")
|
||||||
|
|
||||||
# Defining listener for message events
|
# Defining listener for message events
|
||||||
@self.bot.listener.on_message_event
|
@self.bot.listener.on_message_event
|
||||||
async def wrapper_handle_commands(room, message):
|
async def wrapper_handle_commands(room, message):
|
||||||
await self.handle_commands(room, message) # Calling handle_commands method for incoming messages
|
await self.handle_commands(room, message) # Calling handle_commands method for incoming messages
|
||||||
|
|
||||||
logging.info("Bot is ready and listening for commands...")
|
logging.info("="*40)
|
||||||
|
logging.info("BOT IS READY - ATTEMPTING TO CONNECT TO MATRIX")
|
||||||
|
logging.info("="*40)
|
||||||
|
logging.info(f"Connecting to {MATRIX_URL} as {MATRIX_USER}...")
|
||||||
|
logging.info("(This may take up to 30 seconds...)")
|
||||||
|
|
||||||
self.bot.run() # Running the bot
|
self.bot.run() # Running the bot
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Fatal error running bot: {e}", exc_info=True)
|
logging.error(f"Fatal error during bot startup: {e}", exc_info=True)
|
||||||
sys.exit(1)
|
logging.error("="*40)
|
||||||
|
logging.error("TROUBLESHOOTING SUGGESTIONS:")
|
||||||
|
logging.error("1. Check your internet connection")
|
||||||
|
logging.error("2. Verify MATRIX_URL is correct (should be https://matrix.org)")
|
||||||
|
logging.error("3. Verify MATRIX_USER and MATRIX_PASS are correct")
|
||||||
|
logging.error("4. Check if matrix.org is accessible from your network")
|
||||||
|
logging.error("5. Try increasing timeout in config")
|
||||||
|
logging.error("="*40)
|
||||||
|
raise
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("FUNGUY BOT LAUNCHER")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
print("Creating bot instance...")
|
||||||
bot = FunguyBot() # Creating instance of FunguyBot
|
bot = FunguyBot() # Creating instance of FunguyBot
|
||||||
|
print("Bot instance created. Running bot...")
|
||||||
bot.run() # Running the bot
|
bot.run() # Running the bot
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logging.info("Bot stopped by user")
|
print("\n[!] Bot stopped by user")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(f"\n[!] Fatal error: {e}")
|
||||||
logging.error(f"Unhandled exception: {e}", exc_info=True)
|
logging.error(f"Unhandled exception: {e}", exc_info=True)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -508,6 +508,30 @@ Search Exploit-DB for security vulnerabilities and exploits. Returns detailed in
|
|||||||
<li><code>!arxiv <query></code> - Search for papers (shows abstracts)</li>
|
<li><code>!arxiv <query></code> - Search for papers (shows abstracts)</li>
|
||||||
<li><code>!arxiv list <query></code> - List papers without abstracts</li>
|
<li><code>!arxiv list <query></code> - List papers without abstracts</li>
|
||||||
<li><code>!arxiv category <category></code> - Browse recent papers by category</li>
|
<li><code>!arxiv category <category></code> - Browse recent papers by category</li>
|
||||||
|
<li><code>!ar0; 10px; color: #3b3a30; font-family: monospace; font-size: 12px; background: #f8f8f8; border: 1px solid #c8c8c8; border-radius: 3px; padding: 0 5px; }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"background": "white",
|
||||||
|
"color": "#3b3a30",
|
||||||
|
"font-family": "monospace",
|
||||||
|
"font-size": "12px",
|
||||||
|
"border": "1px solid #c8c8c8",
|
||||||
|
"border-radius": "3px",
|
||||||
|
"padding": "0 5px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="codeblock" style="white-space: pre-line; padding: 10px; border: 1px solid #c8c8c8; border-radius: 3px; background: #f8f8f8; color: #3b3a30; font-family: monospace; font-size: 12px;">
|
||||||
|
</style>
|
||||||
|
<div class="codeblock">
|
||||||
|
<summary>📚 <strong>!arxiv [query]</strong></summary>
|
||||||
|
<p>Search academic papers on arXiv.org. Categories include AI, ML, Security, Physics, Math, and more. No API key required.</p>
|
||||||
|
<p><strong>Commands:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!arxiv <query></code> - Search for papers (shows abstracts)</li>
|
||||||
|
<li><code>!arxiv list <query></code> - List papers without abstracts</li>
|
||||||
|
<li><code>!arxiv category <category></code> - Browse recent papers by category</li>
|
||||||
<li><code>!arxiv recent <category></code> - Most recent papers in category</li>
|
<li><code>!arxiv recent <category></code> - Most recent papers in category</li>
|
||||||
<li><code>!arxiv random</code> - Get a random paper</li>
|
<li><code>!arxiv random</code> - Get a random paper</li>
|
||||||
<li><code>!arxiv <id></code> - Get paper by arXiv ID (e.g., 2101.00101)</li>
|
<li><code>!arxiv <id></code> - Get paper by arXiv ID (e.g., 2101.00101)</li>
|
||||||
@@ -548,6 +572,50 @@ Search Exploit-DB for security vulnerabilities and exploits. Returns detailed in
|
|||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details><summary>☯ <strong>!karma [user]</strong></summary>
|
||||||
|
<p>Track karma points for users with leaderboards and statistics. Supports display names and Matrix IDs.</p>
|
||||||
|
<p><strong>Commands:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!karma <user></code> - Show karma for a user</li>
|
||||||
|
<li><code>!karma++ <user></code> - Give +1 karma to a user</li>
|
||||||
|
<li><code>!karma-- <user></code> - Give -1 karma to a user</li>
|
||||||
|
<li><code>!karma top [n]</code> - Show top karma entries</li>
|
||||||
|
<li><code>!karma bottom [n]</code> - Show bottom karma entries</li>
|
||||||
|
<li><code>!karma rank <user></code> - Show rank of user</li>
|
||||||
|
<li><code>!karma stats</code> - Show overall statistics</li>
|
||||||
|
<li><code>!karma history <user></code> - Show recent karma history</li>
|
||||||
|
<li><code>!++ <user></code> - Shortcut for !karma++</li>
|
||||||
|
<li><code>!-- <user></code> - Shortcut for !karma--</li>
|
||||||
|
<li><code><user>++</code> - Inline karma (message contains ++)</li>
|
||||||
|
<li><code><user>--</code> - Inline karma (message contains --)</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Features:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Supports display names and Matrix IDs</li>
|
||||||
|
<li>Room-specific karma tracking</li>
|
||||||
|
<li>Rate limiting to prevent spam</li>
|
||||||
|
<li>Karma history tracking</li>
|
||||||
|
<li>Leaderboards and statistics</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🔥 <strong>!hn [command]</strong></summary>
|
||||||
|
<p>Fetch top stories from Hacker News using Firebase API. No API key required.</p>
|
||||||
|
<p><strong>Commands:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!hn</code> - Show top 5 stories (default)</li>
|
||||||
|
<li><code>!hn top</code> - Top stories</li>
|
||||||
|
<li><code>!hn new</code> - Newest stories</li>
|
||||||
|
<li><code>!hn best</code> - Best stories</li>
|
||||||
|
<li><code>!hn ask</code> - Ask HN threads</li>
|
||||||
|
<li><code>!hn show</code> - Show HN posts</li>
|
||||||
|
<li><code>!hn job</code> - Job postings</li>
|
||||||
|
<li><code>!hn story <id></code> - Get details of a specific story</li>
|
||||||
|
<li><code>!hn comments <id></code> - Show comments for a story</li>
|
||||||
|
<li><code>!hn search <query></code> - Search stories (via Algolia)</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details><summary>⏱️ <strong>!cron [add|remove] [room_id] [cron_entry] [command]</strong></summary>
|
<details><summary>⏱️ <strong>!cron [add|remove] [room_id] [cron_entry] [command]</strong></summary>
|
||||||
<p>Schedule automated commands using cron syntax. Add or remove cron jobs for specific rooms and commands.</p>
|
<p>Schedule automated commands using cron syntax. Add or remove cron jobs for specific rooms and commands.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
+785
-88
@@ -1,106 +1,803 @@
|
|||||||
"""
|
"""
|
||||||
This plugin provides a command to manage karma points for users.
|
Advanced Karma Plugin for Funguy Bot
|
||||||
|
|
||||||
|
Provides comprehensive karma tracking with leaderboards, trends, and statistics.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
* Give/take karma points from users using display names or Matrix IDs
|
||||||
|
* Track karma history with timestamps
|
||||||
|
* View karma leaderboards (top/bottom)
|
||||||
|
* Rate limiting to prevent spam
|
||||||
|
* Room-specific karma tracking
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
!karma - Show this help
|
||||||
|
!karma <user> - Show karma for a user
|
||||||
|
!karma++ <user> - Give +1 karma
|
||||||
|
!karma-- <user> - Give -1 karma
|
||||||
|
!karma top [n] - Show top karma entries
|
||||||
|
!karma bottom [n] - Show bottom karma entries
|
||||||
|
!karma rank <user> - Show rank of user
|
||||||
|
!karma stats - Show overall statistics
|
||||||
|
!karma history <user>- Show recent karma history
|
||||||
|
|
||||||
|
Shortcuts:
|
||||||
|
!++ <user> - Same as !karma++ <user>
|
||||||
|
!-- <user> - Same as !karma-- <user>
|
||||||
|
<user>++ - Give +1 karma (inline)
|
||||||
|
<user>-- - Give -1 karma (inline)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# plugins/karma.py
|
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import logging
|
import logging
|
||||||
import simplematrixbotlib as botlib
|
import simplematrixbotlib as botlib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
|
||||||
async def handle_command(room, message, bot, prefix, config):
|
# ---------------------------------------------------------------------------
|
||||||
"""
|
# Configuration
|
||||||
Function to handle the !karma command.
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Args:
|
# Prevent spam: minimum seconds between karma changes to same target by same user
|
||||||
room (Room): The Matrix room where the command was invoked.
|
COOLDOWN_SECONDS = 5
|
||||||
message (RoomMessage): The message object containing the command.
|
|
||||||
|
|
||||||
Returns:
|
# Database file
|
||||||
None
|
DB_FILE = "karma.db"
|
||||||
"""
|
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("karma"):
|
|
||||||
logging.info("Received !karma command")
|
|
||||||
args = match.args()
|
|
||||||
sender = str(message.sender)
|
|
||||||
|
|
||||||
if len(args) == 0:
|
# Cache for display name to user ID mappings (per room)
|
||||||
# Query sender's own karma
|
# Structure: {room_id: {display_name: user_id}}
|
||||||
conn = sqlite3.connect('karma.db')
|
display_name_cache = {}
|
||||||
|
|
||||||
|
# Last time we refreshed the cache (per room)
|
||||||
|
cache_timestamp = {}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Database Setup with Room Support
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
"""Initialize the database tables with room support."""
|
||||||
|
logging.info("Initializing karma database...")
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS karma
|
|
||||||
(username TEXT PRIMARY KEY, points INTEGER)''')
|
|
||||||
c.execute('''INSERT OR IGNORE INTO karma (username, points) VALUES (?, ?)''', (sender, 0))
|
|
||||||
c.execute('''SELECT points FROM karma WHERE username = ?''', (sender,))
|
|
||||||
row = c.fetchone()
|
|
||||||
if row is not None:
|
|
||||||
points = row[0]
|
|
||||||
await bot.api.send_markdown_message(room.room_id, f"💗 {sender}'s karma points: {points}")
|
|
||||||
logging.info(f"Sent {sender}'s karma points ({points}) to the room")
|
|
||||||
else:
|
|
||||||
await bot.api.send_markdown_message(room.room_id, f"💗 {sender} does not have any karma points yet.")
|
|
||||||
logging.info(f"Sent message that {sender} does not have any karma points yet.")
|
|
||||||
conn.close()
|
|
||||||
elif len(args) == 1:
|
|
||||||
username = args[0]
|
|
||||||
|
|
||||||
if username == sender:
|
# Create tables (store user_id, not display name)
|
||||||
await bot.api.send_markdown_message(room.room_id, "❌ You cannot modify your own karma.")
|
c.execute('''CREATE TABLE IF NOT EXISTS karma (
|
||||||
logging.info("Sent self-modification warning message to the room")
|
room_id TEXT,
|
||||||
return
|
user_id TEXT,
|
||||||
|
points INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (room_id, user_id)
|
||||||
|
)''')
|
||||||
|
|
||||||
conn = sqlite3.connect('karma.db')
|
c.execute('''CREATE TABLE IF NOT EXISTS karma_history (
|
||||||
c = conn.cursor()
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS karma
|
room_id TEXT,
|
||||||
(username TEXT PRIMARY KEY, points INTEGER)''')
|
user_id TEXT,
|
||||||
c.execute('''INSERT OR IGNORE INTO karma (username, points) VALUES (?, ?)''', (username, 0))
|
change INTEGER,
|
||||||
c.execute('''SELECT points FROM karma WHERE username = ?''', (username,))
|
new_points INTEGER,
|
||||||
row = c.fetchone()
|
voter TEXT,
|
||||||
if row is not None:
|
voted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
points = row[0]
|
)''')
|
||||||
await bot.api.send_markdown_message(room.room_id, f"💗 {username}'s karma points: {points}")
|
|
||||||
logging.info(f"Sent {username}'s karma points ({points}) to the room")
|
|
||||||
else:
|
|
||||||
await bot.api.send_markdown_message(room.room_id, f"💗 {username} does not have any karma points yet.")
|
|
||||||
logging.info(f"Sent message that {username} does not have any karma points yet.")
|
|
||||||
conn.close()
|
|
||||||
elif len(args) == 2:
|
|
||||||
username, action = args
|
|
||||||
if action not in ['up', 'down']:
|
|
||||||
await bot.api.send_markdown_message(room.room_id, "❌ Invalid action. Use 'up' or 'down'.")
|
|
||||||
logging.info("Sent invalid action message to the room")
|
|
||||||
return
|
|
||||||
|
|
||||||
if username == sender:
|
c.execute('''CREATE TABLE IF NOT EXISTS karma_cooldown (
|
||||||
await bot.api.send_markdown_message(room.room_id, "❌ You cannot modify your own karma.")
|
room_id TEXT,
|
||||||
logging.info("Sent self-modification warning message to the room")
|
user_id TEXT,
|
||||||
return
|
voter TEXT,
|
||||||
|
last_voted TIMESTAMP,
|
||||||
conn = sqlite3.connect('karma.db')
|
PRIMARY KEY (room_id, user_id, voter)
|
||||||
c = conn.cursor()
|
)''')
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS karma
|
|
||||||
(username TEXT PRIMARY KEY, points INTEGER)''')
|
|
||||||
|
|
||||||
if action == 'up':
|
|
||||||
c.execute('''INSERT OR IGNORE INTO karma (username, points) VALUES (?, ?)''', (username, 0))
|
|
||||||
c.execute('''UPDATE karma SET points = points + 1 WHERE username = ?''', (username,))
|
|
||||||
else:
|
|
||||||
c.execute('''INSERT OR IGNORE INTO karma (username, points) VALUES (?, ?)''', (username, 0))
|
|
||||||
c.execute('''UPDATE karma SET points = points - 1 WHERE username = ?''', (username,))
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
c.execute('''SELECT points FROM karma WHERE username = ?''', (username,))
|
|
||||||
row = c.fetchone()
|
|
||||||
if row is not None:
|
|
||||||
points = row[0]
|
|
||||||
await bot.api.send_markdown_message(room.room_id, f"💗 {username}'s karma points: {points}")
|
|
||||||
logging.info(f"Sent {username}'s karma points ({points}) to the room")
|
|
||||||
else:
|
|
||||||
await bot.api.send_markdown_message(room.room_id, f"💗 {username} does not have any karma points yet.")
|
|
||||||
logging.info(f"Sent message that {username} does not have any karma points yet.")
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
logging.info("Karma database initialized successfully")
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
"""Get database connection with row factory for dict access."""
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Display Name Resolution
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def refresh_display_name_cache(bot, room_id):
|
||||||
|
"""Refresh the cache of display names to user IDs for a room."""
|
||||||
|
global display_name_cache, cache_timestamp
|
||||||
|
|
||||||
|
# Check if we need to refresh (cache older than 5 minutes)
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
if room_id in cache_timestamp and (now - cache_timestamp[room_id]) < 300:
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info(f"Refreshing display name cache for room {room_id}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to get room members from the bot's state
|
||||||
|
if hasattr(bot, 'async_client') and bot.async_client:
|
||||||
|
# Get the room state
|
||||||
|
room = bot.async_client.rooms.get(room_id)
|
||||||
|
if room and hasattr(room, 'users'):
|
||||||
|
# Build mapping of display names to user IDs
|
||||||
|
name_map = {}
|
||||||
|
for user_id, user_info in room.users.items():
|
||||||
|
# Get display name - try different attributes
|
||||||
|
display_name = None
|
||||||
|
if hasattr(user_info, 'display_name') and user_info.display_name:
|
||||||
|
display_name = user_info.display_name
|
||||||
|
elif hasattr(user_info, 'name') and user_info.name:
|
||||||
|
display_name = user_info.name
|
||||||
|
|
||||||
|
if display_name:
|
||||||
|
name_map[display_name.lower()] = user_id
|
||||||
|
# Also store without emojis for easier matching
|
||||||
|
clean_name = re.sub(r'[^\w\s]', '', display_name).strip().lower()
|
||||||
|
if clean_name and clean_name != display_name.lower():
|
||||||
|
name_map[clean_name] = user_id
|
||||||
|
|
||||||
|
display_name_cache[room_id] = name_map
|
||||||
|
cache_timestamp[room_id] = now
|
||||||
|
logging.info(f"Cached {len(name_map)} display names for room {room_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Could not refresh display name cache: {e}")
|
||||||
|
|
||||||
|
# If we couldn't get members, initialize empty cache
|
||||||
|
display_name_cache[room_id] = {}
|
||||||
|
cache_timestamp[room_id] = now
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_display_name(room_id, display_name, bot=None):
|
||||||
|
"""Resolve a display name to a Matrix user ID."""
|
||||||
|
global display_name_cache
|
||||||
|
|
||||||
|
# If it's already a valid Matrix ID, return it
|
||||||
|
if display_name.startswith('@') and ':' in display_name:
|
||||||
|
return display_name
|
||||||
|
|
||||||
|
# Check the cache
|
||||||
|
if room_id in display_name_cache:
|
||||||
|
name_map = display_name_cache[room_id]
|
||||||
|
|
||||||
|
# Try exact match (case-insensitive)
|
||||||
|
key = display_name.lower()
|
||||||
|
if key in name_map:
|
||||||
|
return name_map[key]
|
||||||
|
|
||||||
|
# Try without emojis/special characters
|
||||||
|
clean_key = re.sub(r'[^\w\s]', '', display_name).strip().lower()
|
||||||
|
if clean_key and clean_key in name_map:
|
||||||
|
return name_map[clean_key]
|
||||||
|
|
||||||
|
# Try partial match (if display name is contained in a cached name)
|
||||||
|
for cached_name, user_id in name_map.items():
|
||||||
|
if key in cached_name or cached_name in key:
|
||||||
|
return user_id
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_display_name_from_user_id(bot, room_id, user_id):
|
||||||
|
"""Get the display name for a user ID."""
|
||||||
|
try:
|
||||||
|
if hasattr(bot, 'async_client') and bot.async_client:
|
||||||
|
room = bot.async_client.rooms.get(room_id)
|
||||||
|
if room and hasattr(room, 'users') and user_id in room.users:
|
||||||
|
user_info = room.users[user_id]
|
||||||
|
if hasattr(user_info, 'display_name') and user_info.display_name:
|
||||||
|
return user_info.display_name
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: extract local part from user ID
|
||||||
|
if ':' in user_id:
|
||||||
|
return user_id.split(':')[0].lstrip('@')
|
||||||
|
return user_id.lstrip('@')
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helper Functions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def is_on_cooldown(room_id, user_id, voter):
|
||||||
|
"""Check if voter is on cooldown for this user in this room."""
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
c.execute('''SELECT last_voted FROM karma_cooldown
|
||||||
|
WHERE room_id = ? AND user_id = ? AND voter = ?''',
|
||||||
|
(room_id, user_id, voter))
|
||||||
|
row = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
try:
|
||||||
|
last_voted = datetime.fromisoformat(row['last_voted'])
|
||||||
|
if datetime.now() - last_voted < timedelta(seconds=COOLDOWN_SECONDS):
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_cooldown_remaining(room_id, user_id, voter):
|
||||||
|
"""Get remaining cooldown seconds for a voter."""
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
c.execute('''SELECT last_voted FROM karma_cooldown
|
||||||
|
WHERE room_id = ? AND user_id = ? AND voter = ?''',
|
||||||
|
(room_id, user_id, voter))
|
||||||
|
row = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
try:
|
||||||
|
last_voted = datetime.fromisoformat(row['last_voted'])
|
||||||
|
elapsed = (datetime.now() - last_voted).total_seconds()
|
||||||
|
remaining = COOLDOWN_SECONDS - elapsed
|
||||||
|
if remaining > 0:
|
||||||
|
return int(remaining)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def update_cooldown(room_id, user_id, voter):
|
||||||
|
"""Update the cooldown timestamp for this voter."""
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
c.execute('''INSERT OR REPLACE INTO karma_cooldown (room_id, user_id, voter, last_voted)
|
||||||
|
VALUES (?, ?, ?, ?)''', (room_id, user_id, voter, datetime.now().isoformat()))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def update_karma(room_id, user_id, change, voter):
|
||||||
|
"""Update karma points for a user in a specific room."""
|
||||||
|
if change == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
logging.debug(f"Updating karma: room={room_id}, user={user_id}, change={change}, voter={voter}")
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
# Insert or ignore the user
|
||||||
|
c.execute('''INSERT OR IGNORE INTO karma (room_id, user_id, points) VALUES (?, ?, 0)''',
|
||||||
|
(room_id, user_id))
|
||||||
|
|
||||||
|
# Update points
|
||||||
|
c.execute('''UPDATE karma SET points = points + ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE room_id = ? AND user_id = ?''', (change, room_id, user_id))
|
||||||
|
|
||||||
|
# Get new points
|
||||||
|
c.execute('''SELECT points FROM karma WHERE room_id = ? AND user_id = ?''', (room_id, user_id))
|
||||||
|
row = c.fetchone()
|
||||||
|
new_points = row['points'] if row else 0
|
||||||
|
|
||||||
|
# Record history
|
||||||
|
c.execute('''INSERT INTO karma_history (room_id, user_id, change, new_points, voter)
|
||||||
|
VALUES (?, ?, ?, ?, ?)''', (room_id, user_id, change, new_points, voter))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logging.debug(f"Karma updated: {user_id} now has {new_points} points in room {room_id}")
|
||||||
|
return new_points
|
||||||
|
|
||||||
|
|
||||||
|
def get_karma(room_id, user_id):
|
||||||
|
"""Get karma points for a user in a specific room."""
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
c.execute('''SELECT points FROM karma WHERE room_id = ? AND user_id = ?''', (room_id, user_id))
|
||||||
|
row = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return row['points'] if row else 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_leaderboard(room_id, bot, limit=10, reverse=False):
|
||||||
|
"""Get top or bottom karma leaderboard for a room."""
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
order = "DESC" if not reverse else "ASC"
|
||||||
|
c.execute(f'''SELECT user_id, points FROM karma
|
||||||
|
WHERE room_id = ? AND points != 0
|
||||||
|
ORDER BY points {order} LIMIT ?''', (room_id, limit))
|
||||||
|
rows = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Convert user IDs to display names
|
||||||
|
leaderboard = []
|
||||||
|
for row in rows:
|
||||||
|
display_name = get_display_name_from_user_id(bot, room_id, row['user_id'])
|
||||||
|
leaderboard.append({
|
||||||
|
'display_name': display_name,
|
||||||
|
'user_id': row['user_id'],
|
||||||
|
'points': row['points']
|
||||||
|
})
|
||||||
|
|
||||||
|
return leaderboard
|
||||||
|
|
||||||
|
|
||||||
|
def get_rank(room_id, user_id):
|
||||||
|
"""Get rank of a user in karma leaderboard for a room."""
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
# Get all points ordered descending
|
||||||
|
c.execute('''SELECT user_id, points FROM karma
|
||||||
|
WHERE room_id = ? AND points != 0
|
||||||
|
ORDER BY points DESC''', (room_id,))
|
||||||
|
rows = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
for i, row in enumerate(rows, 1):
|
||||||
|
if row['user_id'] == user_id:
|
||||||
|
return i, len(rows)
|
||||||
|
return None, len(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def get_recent_history(room_id, user_id, bot, limit=5):
|
||||||
|
"""Get recent karma history for a user in a room."""
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
c.execute('''SELECT change, voter, voted_at
|
||||||
|
FROM karma_history
|
||||||
|
WHERE room_id = ? AND user_id = ?
|
||||||
|
ORDER BY voted_at DESC LIMIT ?''', (room_id, user_id, limit))
|
||||||
|
rows = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
history = []
|
||||||
|
for row in rows:
|
||||||
|
voter_name = get_display_name_from_user_id(bot, room_id, row['voter'])
|
||||||
|
history.append({
|
||||||
|
'change': row['change'],
|
||||||
|
'voter': voter_name,
|
||||||
|
'voter_id': row['voter'],
|
||||||
|
'voted_at': row['voted_at']
|
||||||
|
})
|
||||||
|
|
||||||
|
return history
|
||||||
|
|
||||||
|
|
||||||
|
def get_stats(room_id=None):
|
||||||
|
"""Get overall karma statistics."""
|
||||||
|
conn = get_connection()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
if room_id:
|
||||||
|
c.execute('''SELECT COUNT(*) as total_users FROM karma WHERE room_id = ?''', (room_id,))
|
||||||
|
total_users = c.fetchone()['total_users']
|
||||||
|
|
||||||
|
c.execute('''SELECT SUM(points) as total_points FROM karma WHERE room_id = ?''', (room_id,))
|
||||||
|
total_points = c.fetchone()['total_points'] or 0
|
||||||
|
|
||||||
|
c.execute('''SELECT AVG(points) as avg_points FROM karma WHERE room_id = ?''', (room_id,))
|
||||||
|
avg_points = c.fetchone()['avg_points'] or 0
|
||||||
|
|
||||||
|
c.execute('''SELECT MAX(points) as max_points FROM karma WHERE room_id = ?''', (room_id,))
|
||||||
|
max_points = c.fetchone()['max_points'] or 0
|
||||||
|
|
||||||
|
c.execute('''SELECT MIN(points) as min_points FROM karma WHERE room_id = ?''', (room_id,))
|
||||||
|
min_points = c.fetchone()['min_points'] or 0
|
||||||
|
|
||||||
|
c.execute('''SELECT COUNT(*) as total_votes FROM karma_history WHERE room_id = ?''', (room_id,))
|
||||||
|
total_votes = c.fetchone()['total_votes']
|
||||||
else:
|
else:
|
||||||
await bot.api.send_markdown_message(room.room_id, "☯ Usage: !karma [username] [up/down]")
|
c.execute('''SELECT COUNT(*) as total_users FROM karma''')
|
||||||
logging.info("Sent usage message to the room")
|
total_users = c.fetchone()['total_users']
|
||||||
|
|
||||||
|
c.execute('''SELECT SUM(points) as total_points FROM karma''')
|
||||||
|
total_points = c.fetchone()['total_points'] or 0
|
||||||
|
|
||||||
|
c.execute('''SELECT AVG(points) as avg_points FROM karma''')
|
||||||
|
avg_points = c.fetchone()['avg_points'] or 0
|
||||||
|
|
||||||
|
c.execute('''SELECT MAX(points) as max_points FROM karma''')
|
||||||
|
max_points = c.fetchone()['max_points'] or 0
|
||||||
|
|
||||||
|
c.execute('''SELECT MIN(points) as min_points FROM karma''')
|
||||||
|
min_points = c.fetchone()['min_points'] or 0
|
||||||
|
|
||||||
|
c.execute('''SELECT COUNT(*) as total_votes FROM karma_history''')
|
||||||
|
total_votes = c.fetchone()['total_votes']
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_users': total_users,
|
||||||
|
'total_points': total_points,
|
||||||
|
'avg_points': round(avg_points, 2),
|
||||||
|
'max_points': max_points,
|
||||||
|
'min_points': min_points,
|
||||||
|
'total_votes': total_votes
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_karma_display(display_name, points):
|
||||||
|
"""Format karma display with visual indicators."""
|
||||||
|
if points > 0:
|
||||||
|
indicator = "👍" if points > 10 else "➕"
|
||||||
|
return f"💗 **{display_name}** has {points} karma {indicator}"
|
||||||
|
elif points < 0:
|
||||||
|
indicator = "👎" if points < -10 else "➖"
|
||||||
|
return f"💔 **{display_name}** has {points} karma {indicator}"
|
||||||
|
else:
|
||||||
|
return f"⚖️ **{display_name}** has neutral karma (0)"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Command Handlers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""Handle karma commands."""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
room_id = room.room_id
|
||||||
|
|
||||||
|
# Refresh display name cache
|
||||||
|
await refresh_display_name_cache(bot, room_id)
|
||||||
|
|
||||||
|
# Debug logging
|
||||||
|
message_body = message.body if hasattr(message, 'body') else str(message)
|
||||||
|
logging.info(f"Karma plugin received message: '{message_body}' from {message.sender}")
|
||||||
|
|
||||||
|
# Get the full command (including what might be karma++ etc.)
|
||||||
|
full_cmd = match.command() if hasattr(match, 'command') else ''
|
||||||
|
|
||||||
|
logging.debug(f"Full command: '{full_cmd}'")
|
||||||
|
|
||||||
|
# Check for !karma++ or !karma-- as a single command
|
||||||
|
if match.is_not_from_this_bot() and match.prefix():
|
||||||
|
if full_cmd == 'karma++':
|
||||||
|
# !karma++ username
|
||||||
|
args = match.args()
|
||||||
|
if not args:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, "Usage: !karma++ <username>")
|
||||||
|
return
|
||||||
|
display_name = ' '.join(args)
|
||||||
|
await process_karma_vote(room, display_name, '++', message.sender, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif full_cmd == 'karma--':
|
||||||
|
# !karma-- username
|
||||||
|
args = match.args()
|
||||||
|
if not args:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, "Usage: !karma-- <username>")
|
||||||
|
return
|
||||||
|
display_name = ' '.join(args)
|
||||||
|
await process_karma_vote(room, display_name, '--', message.sender, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif full_cmd == 'karma':
|
||||||
|
# Regular !karma command
|
||||||
|
args = match.args()
|
||||||
|
if args and args[0] in ['++', '--']:
|
||||||
|
# !karma ++ username (space between)
|
||||||
|
action = args[0]
|
||||||
|
if len(args) < 2:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"Usage: !karma {action} <username>")
|
||||||
|
return
|
||||||
|
display_name = ' '.join(args[1:])
|
||||||
|
await process_karma_vote(room, display_name, action, message.sender, bot)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# !karma with subcommands or username
|
||||||
|
await handle_karma_command(room, message, bot, config)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle !++ and !-- shortcuts
|
||||||
|
elif full_cmd in ['++', '--']:
|
||||||
|
args = match.args()
|
||||||
|
if not args:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"Usage: !{full_cmd} <username>\nExample: !{full_cmd} Nexilva")
|
||||||
|
return
|
||||||
|
display_name = ' '.join(args)
|
||||||
|
await process_karma_vote(room, display_name, full_cmd, message.sender, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle inline karma (message body contains ++ or --)
|
||||||
|
if match.is_not_from_this_bot() and not match.prefix():
|
||||||
|
await handle_inline_karma(room, message, bot)
|
||||||
|
|
||||||
|
|
||||||
|
async def process_karma_vote(room, display_name, action, voter, bot):
|
||||||
|
"""Process a karma vote using display name."""
|
||||||
|
room_id = room.room_id
|
||||||
|
voter_str = str(voter)
|
||||||
|
change = 1 if action == '++' else -1
|
||||||
|
|
||||||
|
logging.info(f"Karma vote: {voter_str} -> '{display_name}' ({action}, change={change:+d}) in room {room_id}")
|
||||||
|
|
||||||
|
# Resolve display name to user ID
|
||||||
|
user_id = resolve_display_name(room_id, display_name, bot)
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"❌ Could not find user '{display_name}'. Please use their display name or Matrix ID (e.g., @username:server)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prevent self-modification
|
||||||
|
if user_id == voter_str:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, "❌ You cannot modify your own karma!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check cooldown
|
||||||
|
if is_on_cooldown(room_id, user_id, voter_str):
|
||||||
|
remaining = get_cooldown_remaining(room_id, user_id, voter_str)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"⏳ You're doing that too fast! Wait {remaining} seconds.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update karma
|
||||||
|
try:
|
||||||
|
new_points = update_karma(room_id, user_id, change, voter_str)
|
||||||
|
update_cooldown(room_id, user_id, voter_str)
|
||||||
|
|
||||||
|
# Get display name for response
|
||||||
|
display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id)
|
||||||
|
response = format_karma_display(display_name_resolved, new_points)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, response)
|
||||||
|
logging.info(f"Karma updated successfully: {user_id} now has {new_points} points")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error updating karma: {e}")
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"❌ Error updating karma: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_karma_command(room, message, bot, config):
|
||||||
|
"""Handle the !karma command and its subcommands."""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, config.prefix)
|
||||||
|
args = match.args()
|
||||||
|
room_id = room.room_id
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
# Show help for !karma command in collapsible details tag with proper HTML lists
|
||||||
|
help_text = f"""<details>
|
||||||
|
<summary>💗 <strong>Karma Plugin Help</strong> (click to expand)</summary>
|
||||||
|
|
||||||
|
<strong>Commands:</strong>
|
||||||
|
<ul>
|
||||||
|
<li><code>!karma</code> - Show this help</li>
|
||||||
|
<li><code>!karma <user></code> - Show karma for a user (use display name or @user:server)</li>
|
||||||
|
<li><code>!karma++ <user></code> - Give +1 karma to a user</li>
|
||||||
|
<li><code>!karma-- <user></code> - Give -1 karma to a user</li>
|
||||||
|
<li><code>!karma top [n]</code> - Show top karma leaders</li>
|
||||||
|
<li><code>!karma bottom [n]</code> - Show bottom karma entries</li>
|
||||||
|
<li><code>!karma rank <user></code> - Show rank of a user</li>
|
||||||
|
<li><code>!karma stats</code> - Show overall statistics</li>
|
||||||
|
<li><code>!karma history <user></code> - Show recent karma history</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<strong>Shortcuts:</strong>
|
||||||
|
<ul>
|
||||||
|
<li><code>!++ <user></code> - Same as !karma++ <user></li>
|
||||||
|
<li><code>!-- <user></code> - Same as !karma-- <user></li>
|
||||||
|
<li><code><user>++</code> - Give +1 karma (inline)</li>
|
||||||
|
<li><code><user>--</code> - Give -1 karma (inline)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<strong>Examples:</strong>
|
||||||
|
<ul>
|
||||||
|
<li><code>!karma Nexilva</code> - Check Nexilva's karma</li>
|
||||||
|
<li><code>!karma++ @hb:matrix.org</code> - Give HB +1 karma</li>
|
||||||
|
<li><code>!karma top 5</code> - Show top 5 leaders</li>
|
||||||
|
<li><code>Nexilva++</code> - Give Nexilva +1 karma (inline)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<strong>Notes:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>You cannot modify your own karma</li>
|
||||||
|
<li>There is a {COOLDOWN_SECONDS} second cooldown between votes</li>
|
||||||
|
<li>Karma is tracked separately per room</li>
|
||||||
|
<li>Display names with emojis are supported</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</details>"""
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, help_text)
|
||||||
|
return
|
||||||
|
|
||||||
|
subcommand = args[0].lower()
|
||||||
|
|
||||||
|
# !karma top [n]
|
||||||
|
if subcommand == "top":
|
||||||
|
limit = 10
|
||||||
|
if len(args) > 1 and args[1].isdigit():
|
||||||
|
limit = min(int(args[1]), 25)
|
||||||
|
|
||||||
|
leaderboard = get_leaderboard(room_id, bot, limit, reverse=False)
|
||||||
|
if leaderboard:
|
||||||
|
response = f"🏆 **Top {len(leaderboard)} Karma Leaders**\n\n"
|
||||||
|
for i, entry in enumerate(leaderboard, 1):
|
||||||
|
medal = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else "📌"
|
||||||
|
response += f"{medal} **{i}.** {entry['display_name']}: {entry['points']} points\n"
|
||||||
|
await bot.api.send_markdown_message(room.room_id, response)
|
||||||
|
else:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, "No karma entries found in this room.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# !karma bottom [n]
|
||||||
|
if subcommand == "bottom":
|
||||||
|
limit = 10
|
||||||
|
if len(args) > 1 and args[1].isdigit():
|
||||||
|
limit = min(int(args[1]), 25)
|
||||||
|
|
||||||
|
leaderboard = get_leaderboard(room_id, bot, limit, reverse=True)
|
||||||
|
if leaderboard:
|
||||||
|
response = f"📉 **Bottom {len(leaderboard)} Karma (Needs Love)**\n\n"
|
||||||
|
for i, entry in enumerate(leaderboard, 1):
|
||||||
|
response += f"⚠️ **{i}.** {entry['display_name']}: {entry['points']} points\n"
|
||||||
|
await bot.api.send_markdown_message(room.room_id, response)
|
||||||
|
else:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, "No karma entries found in this room.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# !karma rank <user>
|
||||||
|
if subcommand == "rank" and len(args) >= 2:
|
||||||
|
display_name = ' '.join(args[1:])
|
||||||
|
user_id = resolve_display_name(room_id, display_name, bot)
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"❌ Could not find user '{display_name}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
rank, total = get_rank(room_id, user_id)
|
||||||
|
if rank:
|
||||||
|
points = get_karma(room_id, user_id)
|
||||||
|
percentile = round((1 - rank/total) * 100, 1) if total > 0 else 0
|
||||||
|
display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id)
|
||||||
|
response = f"📊 **{display_name_resolved}** is ranked #{rank} out of {total} (top {percentile}%)\n💗 Karma: {points} points"
|
||||||
|
await bot.api.send_markdown_message(room.room_id, response)
|
||||||
|
else:
|
||||||
|
display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"❌ {display_name_resolved} has no karma yet in this room.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# !karma history <user>
|
||||||
|
if subcommand == "history" and len(args) >= 2:
|
||||||
|
display_name = ' '.join(args[1:])
|
||||||
|
user_id = resolve_display_name(room_id, display_name, bot)
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"❌ Could not find user '{display_name}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
history = get_recent_history(room_id, user_id, bot, 5)
|
||||||
|
|
||||||
|
if history:
|
||||||
|
display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id)
|
||||||
|
response = f"📜 **Recent Karma History for {display_name_resolved}**\n\n"
|
||||||
|
for h in history:
|
||||||
|
arrow = "⬆️" if h['change'] > 0 else "⬇️"
|
||||||
|
try:
|
||||||
|
voted_at = datetime.fromisoformat(h['voted_at'])
|
||||||
|
time_str = voted_at.strftime("%Y-%m-%d %H:%M")
|
||||||
|
except:
|
||||||
|
time_str = "recently"
|
||||||
|
response += f"{arrow} {h['change']:+d} by **{h['voter']}** at {time_str}\n"
|
||||||
|
await bot.api.send_markdown_message(room.room_id, response)
|
||||||
|
else:
|
||||||
|
display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"No karma history for {display_name_resolved} in this room.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# !karma stats
|
||||||
|
if subcommand == "stats":
|
||||||
|
stats = get_stats(room_id)
|
||||||
|
response = f"📊 **Karma System Statistics for this Room**\n\n"
|
||||||
|
response += f"📝 Total users tracked: {stats['total_users']}\n"
|
||||||
|
response += f"⭐ Total karma points: {stats['total_points']}\n"
|
||||||
|
response += f"📈 Average karma: {stats['avg_points']}\n"
|
||||||
|
response += f"🏆 Highest karma: {stats['max_points']}\n"
|
||||||
|
response += f"📉 Lowest karma: {stats['min_points']}\n"
|
||||||
|
response += f"🗳️ Total votes cast: {stats['total_votes']}\n"
|
||||||
|
await bot.api.send_markdown_message(room.room_id, response)
|
||||||
|
return
|
||||||
|
|
||||||
|
# !karma <user> (show karma for specific user)
|
||||||
|
display_name = ' '.join(args)
|
||||||
|
user_id = resolve_display_name(room_id, display_name, bot)
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"❌ Could not find user '{display_name}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
points = get_karma(room_id, user_id)
|
||||||
|
display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id)
|
||||||
|
response = format_karma_display(display_name_resolved, points)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, response)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_inline_karma(room, message, bot):
|
||||||
|
"""Handle inline karma expressions like 'user++' or 'user--'."""
|
||||||
|
body = message.body if hasattr(message, 'body') else str(message)
|
||||||
|
sender = str(message.sender)
|
||||||
|
room_id = room.room_id
|
||||||
|
|
||||||
|
# Refresh display name cache
|
||||||
|
await refresh_display_name_cache(bot, room_id)
|
||||||
|
|
||||||
|
# Pattern to match text followed by ++ or -- at end of word/string
|
||||||
|
pattern = r'(.+?)(\+\+|--)(?:\s|$)'
|
||||||
|
matches = re.findall(pattern, body)
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info(f"Found inline karma matches: {matches}")
|
||||||
|
|
||||||
|
responses = []
|
||||||
|
for display_name, operator in matches:
|
||||||
|
display_name = display_name.strip()
|
||||||
|
change = 1 if operator == '++' else -1
|
||||||
|
|
||||||
|
# Resolve display name to user ID
|
||||||
|
user_id = resolve_display_name(room_id, display_name, bot)
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
logging.debug(f"Could not resolve display name: '{display_name}'")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip self-modification
|
||||||
|
if user_id == sender:
|
||||||
|
logging.debug(f"Skipping self-modification: {sender} -> {display_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check cooldown
|
||||||
|
if is_on_cooldown(room_id, user_id, sender):
|
||||||
|
logging.debug(f"Cooldown active for {sender} -> {user_id}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update karma
|
||||||
|
try:
|
||||||
|
new_points = update_karma(room_id, user_id, change, sender)
|
||||||
|
update_cooldown(room_id, user_id, sender)
|
||||||
|
|
||||||
|
# Format response
|
||||||
|
display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id)
|
||||||
|
arrow = "⬆️" if change > 0 else "⬇️"
|
||||||
|
responses.append(f"{arrow} **{display_name_resolved}** → {new_points}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error updating inline karma: {e}")
|
||||||
|
|
||||||
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
if responses:
|
||||||
|
response_text = " | ".join(responses[:3])
|
||||||
|
if len(responses) > 3:
|
||||||
|
response_text += f" (and {len(responses)-3} more)"
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"💗 {response_text}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Plugin Setup
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def setup(bot):
|
||||||
|
"""Initialize the karma plugin."""
|
||||||
|
logging.info("=" * 50)
|
||||||
|
logging.info("LOADING KARMA PLUGIN")
|
||||||
|
logging.info("=" * 50)
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
logging.info("Advanced Karma plugin loaded successfully")
|
||||||
|
logging.info("Supports display names (e.g., 'Nexilva' or '🍄 HB🍄') and Matrix IDs")
|
||||||
|
logging.info("Commands: !karma, !karma++, !karma--, !++, !--, and inline ++/--")
|
||||||
|
logging.info("=" * 50)
|
||||||
|
|||||||
Reference in New Issue
Block a user