Updated karma 1 hour per target rate limit. Updated requirements.txt and README.md
This commit is contained in:
@@ -1,76 +1,147 @@
|
|||||||
I wrote this bot in one night, while I'm recovering from two level cervical spinal surgery, CDA Cervical Discectomy and Disc Arthroplasty. Expect a lot of bugs.
|
# 🍄 Funguy Bot
|
||||||
|
|
||||||
# Matrix Bot
|
A modular, security-focused Matrix chat bot built with [`simplematrixbotlib`](https://simple-matrix-bot-lib.readthedocs.io/), written in Python. Features a plugin architecture, in-process cron scheduling, per-room plugin management, rate limiting, and a full suite of recon, encoding, and utility commands.
|
||||||
|
|
||||||
Matrix Bot is a Python-based chat bot designed to work with Matrix, an open network for secure, decentralized communication. This bot is built using the `simplematrixbotlib` library and provides various commands and functionalities for interacting with Matrix rooms.
|
> Written during recovery from cervical spinal surgery (CDA – Cervical Discectomy and Disc Arthroplasty). Expect bugs — please report them!
|
||||||
|
|
||||||
## Features
|
---
|
||||||
|
|
||||||
- Modular architecture: Commands are implemented as separate plugins, making it easy to add or modify functionality.
|
## 📋 Table of Contents
|
||||||
- Command handling: The bot listens for specific commands prefixed with `!` and responds accordingly.
|
|
||||||
- Plugin system: Each command is implemented as a separate plugin module, allowing for easy customization and extension.
|
|
||||||
- Extensible: Users can add new commands by creating additional plugin modules.
|
|
||||||
|
|
||||||
## Automatic Installation
|
- [Features](#-features)
|
||||||
Run the installation script
|
- [Requirements](#-requirements)
|
||||||
1. `./install-funguy.sh`
|
- [Installation](#-installation)
|
||||||
|
- [Configuration](#-configuration)
|
||||||
|
- [Running the Bot](#-running-the-bot)
|
||||||
|
- [Core Admin Commands](#-core-admin-commands)
|
||||||
|
- [Plugin Reference](#-plugin-reference)
|
||||||
|
- [Rate Limiting](#-rate-limiting)
|
||||||
|
- [Security Notes](#-security-notes)
|
||||||
|
- [Support](#-support)
|
||||||
|
- [Credits](#-credits)
|
||||||
|
|
||||||
2. Launch the bot:
|
---
|
||||||
`sudo systemctl start funguybot`
|
|
||||||
|
|
||||||
## Manual Installation
|
## ✨ Features
|
||||||
|
|
||||||
1. Create python venv
|
- **Modular plugin system** – each command lives in its own `plugins/*.py` file; add or remove features without touching core code
|
||||||
`python3 -m venv venv`
|
- **Runtime plugin management** – load, unload, enable, or disable plugins per-room with no restart required
|
||||||
`source venv/bin/activate`
|
- **In-process cron scheduler** – schedule any bot command to fire automatically (APScheduler + SQLite, no system crontab needed)
|
||||||
|
- **Rate limiting** – non-admin users are capped at 3 commands per 5-second window; admins are always exempt
|
||||||
|
- **SSRF protection** – all outbound network plugins validate that targets resolve to public IPs only
|
||||||
|
- **Admin/moderator access control** – a dedicated `admin_user` is set in config; moderation commands require power level ≥ 50
|
||||||
|
- **Live configuration** – settings can be changed at runtime with `!set`/`!saveconf` without restarting
|
||||||
|
|
||||||
2. Clone the repository:
|
---
|
||||||
`git clone https://gitlab.com/Eggzy/funguybot.git`
|
|
||||||
|
|
||||||
3. Apply the patch
|
## 📦 Requirements
|
||||||
`cp api.py.patch simplematrixbotlib`
|
|
||||||
`git apply api.py.patch`
|
|
||||||
|
|
||||||
4. Install dependencies:
|
- Python 3.9+
|
||||||
`cd simplematrixbotlib && pip install .`
|
- A Matrix homeserver account for the bot
|
||||||
`cd ../ && pip install -r requirements.txt`
|
- `fortune` package installed on the host (`sudo apt install fortune`) if using `fortune.py`
|
||||||
|
- `dict` / WordNet installed on the host (`sudo apt install dict dict-wn`) if using `dictionary.py`
|
||||||
|
- Optional API keys for certain plugins (see [Configuration](#-configuration))
|
||||||
|
|
||||||
5. If you use the Goodreads quote plugin (quote.py):
|
---
|
||||||
|
|
||||||
`pip install playwright beautifulsoup4 lxml`
|
## 🚀 Installation
|
||||||
`playwright install chromium`
|
|
||||||
|
|
||||||
6. Set up environment variables:
|
**1. Clone the repository**
|
||||||
Create/Edit `.env` file in the root directory of the bot and add the following variables:
|
|
||||||
|
|
||||||
```
|
```bash
|
||||||
MATRIX_URL="https://matrix.org" (or another homeserver)
|
git clone https://gitlab.com/Eggzy/funguybot.git
|
||||||
MATRIX_USER=""
|
cd funguybot
|
||||||
MATRIX_PASS=""
|
|
||||||
OPENWEATHER_API_KEY="" # Optional: For weather plugin
|
|
||||||
|
|
||||||
SMTP_SERVER = "example.com`"
|
|
||||||
SMTP_PORT = 465
|
|
||||||
SMTP_USER = "name@domain.tld"
|
|
||||||
SMTP_PASSWORD = "somepassword"
|
|
||||||
|
|
||||||
OPENWEATHER_API_KEY=
|
|
||||||
SHODAN_KEY=
|
|
||||||
DNSDUMPSTER_KEY=
|
|
||||||
LASTFM_API_KEY=
|
|
||||||
YOUTUBE_API_KEY=
|
|
||||||
|
|
||||||
INFERMATIC_API=
|
|
||||||
INFERMATIC_MODEL=
|
|
||||||
OMDB_API_KEY=
|
|
||||||
GNEWS_API_KEY=
|
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Create systemd.service
|
**2. Create and activate a Python virtual environment**
|
||||||
Create `/etc/systemd/system/funguybot.service`
|
|
||||||
Replace `$working_directory` with your bot install path
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**3. Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. (Optional) Install Playwright for the Goodreads quote plugin (`quote.py`)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip3 install playwright beautifulsoup4 lxml
|
||||||
|
playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Configure the bot** — create your `.env` and `funguy.conf` files (see [Configuration](#-configuration))
|
||||||
|
|
||||||
|
**6. Set up and start the systemd service** (see [Running the Bot](#-running-the-bot))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
### Environment Variables (`.env`)
|
||||||
|
|
||||||
|
Create a `.env` file in the bot's root directory. Only the three Matrix variables are required; all API keys are optional.
|
||||||
|
|
||||||
|
```env
|
||||||
|
# ── Required ──────────────────────────────────────────────────────────
|
||||||
|
MATRIX_URL="https://matrix.org" # Your homeserver URL
|
||||||
|
MATRIX_USER="@yourbot:matrix.org" # Bot's Matrix ID
|
||||||
|
MATRIX_PASS="your_password" # Bot's password
|
||||||
|
|
||||||
|
# ── Logging (optional, default: INFO) ─────────────────────────────────
|
||||||
|
LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR | CRITICAL
|
||||||
|
|
||||||
|
# ── Plugin API Keys (all optional) ────────────────────────────────────
|
||||||
|
OPENWEATHER_API_KEY= # weather.py
|
||||||
|
SHODAN_KEY= # shodan.py
|
||||||
|
DNSDUMPSTER_KEY= # dnsdumpster.py
|
||||||
|
LASTFM_API_KEY= # lastfm.py
|
||||||
|
YOUTUBE_API_KEY= # youtube-search.py
|
||||||
|
INFERMATIC_API= # infermatic-text.py
|
||||||
|
INFERMATIC_MODEL= # infermatic-text.py – model name string
|
||||||
|
OMDB_API_KEY= # imdb.py
|
||||||
|
GNEWS_API_KEY= # news.py
|
||||||
|
|
||||||
|
# ── SMTP (optional, for notification plugins) ─────────────────────────
|
||||||
|
SMTP_SERVER=mail.example.com
|
||||||
|
SMTP_PORT=465
|
||||||
|
SMTP_USER=bot@example.com
|
||||||
|
SMTP_PASSWORD=yourpassword
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bot Configuration (`funguy.conf`)
|
||||||
|
|
||||||
|
Create `funguy.conf` in the bot's root directory. This is a TOML file consumed by `simplematrixbotlib` and the `config.py` plugin.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[simplematrixbotlib.config]
|
||||||
|
admin_user = "@youradmin:matrix.org" # Full Matrix ID of the admin
|
||||||
|
prefix = "!" # Single-character command prefix
|
||||||
|
join_on_invite = true
|
||||||
|
encryption_enabled = false
|
||||||
|
emoji_verify = false
|
||||||
|
ignore_unverified_devices = true
|
||||||
|
store_path = "./store/"
|
||||||
|
|
||||||
|
# Pre-disable plugins per room (optional)
|
||||||
|
[plugins.disabled]
|
||||||
|
# "!yourroom:matrix.org" = ["pluginname1", "pluginname2"]
|
||||||
|
```
|
||||||
|
|
||||||
|
All writable options can also be changed live by the admin using `!set` (see [config.py](#configpy--live-configuration-admin-only)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 Running the Bot
|
||||||
|
|
||||||
|
### Systemd Service (Recommended)
|
||||||
|
|
||||||
|
Create `/etc/systemd/system/funguybot.service`, replacing the `$variables` with your actual paths and user:
|
||||||
|
|
||||||
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Funguy Bot Service
|
Description=Funguy Bot Service
|
||||||
After=network.target
|
After=network.target
|
||||||
@@ -88,101 +159,678 @@ SyslogIdentifier=funguybot
|
|||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
|
||||||
```
|
```
|
||||||
|
|
||||||
8. Launch Fungy
|
```bash
|
||||||
```
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable funguybot
|
systemctl enable funguybot
|
||||||
systemctl start funguybot
|
systemctl start funguybot
|
||||||
```
|
```
|
||||||
|
|
||||||
# 🍄 Funguy Bot Commands 🍄
|
### Direct Launch
|
||||||
|
|
||||||
## Available Plugins
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
The bot includes the following plugins:
|
python3 funguy.py
|
||||||
|
```
|
||||||
- **admin.py**: Full room moderation – multi-word name support
|
|
||||||
- **arxiv.py**: arXiv academic paper search
|
|
||||||
- **bitcoin.py**: Current Bitcoin price
|
|
||||||
- **common.py**: Shared utilities for FunguyBot plugins.
|
|
||||||
- **config.py**: Admin-only configuration commands (preserves disabled plugins)
|
|
||||||
- **cron.py**: In-process cron scheduler (room-aware, no system crontab)
|
|
||||||
- **date.py**: Show current date and time
|
|
||||||
- **ddg.py**: DuckDuckGo search plugin
|
|
||||||
- **dictionary.py**: Look up word definitions using system dictionary
|
|
||||||
- **dns.py**: DNS reconnaissance (SSRF-safe)
|
|
||||||
- **dnsdumpster.py**: DNSDumpster domain reconnaissance
|
|
||||||
- **encode.py**: Comprehensive CyberChef-like encoding and analysis toolkit
|
|
||||||
- **exploitdb.py**: Exploit-DB search
|
|
||||||
- **fortune.py**: Random fortune message
|
|
||||||
- **geo.py**: IP geolocation lookup
|
|
||||||
- **hackernews.py**: Hacker News integration
|
|
||||||
- **hashid.py**: Hash type identifier
|
|
||||||
- **headers.py**: HTTP security header analysis
|
|
||||||
- **help.py**: Plugin for dynamically aggregating help from all loaded plugins.
|
|
||||||
- **imdb.py**: IMDb lookup via OMDb API
|
|
||||||
- **infermatic-text.py**: AI text generation via Infermatic API
|
|
||||||
- **isup.py**: Check if a site is up
|
|
||||||
- **joke.py**: Get random jokes from the Official Joke API
|
|
||||||
- **karma.py**: Room karma tracking system (display names only, no Matrix IDs)
|
|
||||||
- **lastfm.py**: Last.fm music stats with aligned code block output
|
|
||||||
- **loadplugin.py**: Load/unload plugins at runtime
|
|
||||||
- **news.py**: News headlines via GNews API
|
|
||||||
- **plugins.py**: List all loaded plugins with count
|
|
||||||
- **proxy.py**: Working SOCKS5 proxy finder
|
|
||||||
- **quote.py**: Fetch Goodreads quotes
|
|
||||||
- **roomstats.py**: Per-user room statistics
|
|
||||||
- **shodan.py**: Shodan.io reconnaissance
|
|
||||||
- **sslscan.py**: SSL/TLS security scanner
|
|
||||||
- **stable-diffusion.py**: Stable Diffusion image generation (LORA support)
|
|
||||||
- **subdomains.py**: Subdomain enumeration via CertSpotter
|
|
||||||
- **subnet.py**: Subnet calculator
|
|
||||||
- **sysinfo.py**: System information plugin
|
|
||||||
- **timezone.py**: World clock (offline IANA zones + free geocoding)
|
|
||||||
- **urbandictionary.py**: Urban Dictionary definitions
|
|
||||||
- **utils.py**: Security utilities for Funguy Bot plugins.
|
|
||||||
- **weather.py**: Weather data plugin
|
|
||||||
- **welcome.py**: Room welcome message
|
|
||||||
- **whois.py**: Domain WHOIS lookup
|
|
||||||
- **wikipedia.py**: Wikipedia article summary
|
|
||||||
- **xkcd.py**: Fetch random or specific xkcd comics
|
|
||||||
- **youtube-search.py**: YouTube video search
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The bot uses a TOML configuration file (`funguy.conf`) for settings:
|
|
||||||
- `admin_user` - Matrix user ID with admin privileges
|
|
||||||
- `prefix` - Command prefix (default: "!")
|
|
||||||
- Plugin-specific settings in `plugins/ai.json` for AI commands
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Python 3.7+
|
|
||||||
- simplematrixbotlib
|
|
||||||
- Various AI/ML services (Stable Diffusion, Ollama, etc.)
|
|
||||||
- Database support (SQLite)
|
|
||||||
- External APIs (OpenWeatherMap, Urban Dictionary, YouTube)
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Ensure all environment variables are set correctly
|
|
||||||
- Check that required services are running (Stable Diffusion API, Ollama, etc.)
|
|
||||||
- Verify plugin permissions and whitelist settings
|
|
||||||
- Check logs for detailed error information
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Join our Matrix room for support and community:
|
|
||||||
[Self-hosting | Security | Sysadmin | Homelab | Programming](https://matrix.to/#/#selfhosting:mozilla.org)
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
**🧙♂️ Creator & Developer**: HB (@hashborgir:mozilla.org)
|
|
||||||
**🍄 Funguy Bot** - Created during recovery from cervical spinal surgery
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Note: This bot was created rapidly and may contain bugs. Please report issues and contribute improvements!*
|
## 🔑 Core Admin Commands
|
||||||
|
|
||||||
|
These commands are handled by `funguy.py` itself and require `admin_user` privileges.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!reload` | Reload all plugins from disk (no restart needed) |
|
||||||
|
| `!load <plugin>` | Dynamically load a single plugin by filename (without `.py`) |
|
||||||
|
| `!unload <plugin>` | Dynamically unload a single plugin |
|
||||||
|
| `!enable <plugin>` | Re-enable a plugin in the current room |
|
||||||
|
| `!disable <plugin>` | Disable a plugin in the current room (persisted to `funguy.conf`) |
|
||||||
|
| `!restart` | Gracefully stop the bot process (systemd will restart it automatically) |
|
||||||
|
| `!rehash` | Reload `funguy.conf` and the disabled-plugin list without restarting |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 Plugin Reference
|
||||||
|
|
||||||
|
All commands use the prefix defined in `funguy.conf` (default `!`). Plugin filenames correspond to the names used with `!load` / `!unload` / `!disable` / `!enable`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🛡️ admin.py – Room Moderation
|
||||||
|
|
||||||
|
Full moderator toolkit with multi-word display name resolution. Requires power level ≥ 50 or `admin_user`. The bot automatically resolves multi-word display names and prompts with a numbered disambiguation list when names are ambiguous.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!kick <name\|@user> [reason]` | Kick a user from the room |
|
||||||
|
| `!ban <name\|@user> [reason]` | Ban a user |
|
||||||
|
| `!unban <@user:domain>` | Unban a user (full MXID required) |
|
||||||
|
| `!invite <name\|@user>` | Invite a user to the room |
|
||||||
|
| `!op <name\|@user> [power_level]` | Promote a user (max 50 / moderator level) |
|
||||||
|
| `!deop <name\|@user>` | Demote a user to power level 0 |
|
||||||
|
| `!userinfo <name\|@user>` | Show display name and power level |
|
||||||
|
| `!topic [new topic]` | View or set the room topic |
|
||||||
|
| `!roomname [new name]` | View or set the room name |
|
||||||
|
| `!avatar [mxc://…]` | View or set the room avatar (must be an `mxc://` URL) |
|
||||||
|
| `!members` | List all joined members with power levels |
|
||||||
|
| `!bans` | List all banned users |
|
||||||
|
| `!modhelp` | Show moderator command reference |
|
||||||
|
|
||||||
|
`!admin <action>` also works as an explicit parent command for all of the above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📄 arxiv.py – arXiv Academic Search
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!arxiv <query>` | Search papers with abstracts |
|
||||||
|
| `!arxiv list <query>` | Search titles only (no abstracts) |
|
||||||
|
| `!arxiv category <cat>` | Browse recent papers by category |
|
||||||
|
| `!arxiv recent [category]` | Papers from the last 7 days |
|
||||||
|
| `!arxiv random` | Random paper |
|
||||||
|
| `!arxiv <id>` | Fetch paper by arXiv ID (e.g. `2101.00101`) |
|
||||||
|
|
||||||
|
**Categories:** `ai`, `ml`, `security`, `crypto`, `cv`, `nlp`, `math`, `physics`, `quantum`, `bio`, `software`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ₿ bitcoin.py – Bitcoin Price
|
||||||
|
|
||||||
|
```
|
||||||
|
!btc
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetches the latest BTC/USD price from `bitcointicker.co` (Bitstamp feed, 60-second intervals). No API key required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚙️ config.py – Live Configuration *(Admin Only)*
|
||||||
|
|
||||||
|
Manage bot settings at runtime. Changes are in-memory until you run `!saveconf`.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!set <option> <value>` | Change a configuration option |
|
||||||
|
| `!get <option>` | View the current value of one option |
|
||||||
|
| `!show` | Display all current configuration values |
|
||||||
|
| `!saveconf` | Write current settings to `funguy.conf` |
|
||||||
|
| `!loadconf` | Reload settings from `funguy.conf` |
|
||||||
|
| `!reset` | Reset all options to defaults (preserves `admin_user`) |
|
||||||
|
| `!config help` | Show config command help |
|
||||||
|
|
||||||
|
**Configurable options:** `prefix`, `timeout`, `join_on_invite`, `encryption_enabled`, `emoji_verify`, `ignore_unverified_devices`, `store_path`, `allowlist`, `blocklist`
|
||||||
|
|
||||||
|
> `admin_user` is read-only via `!set` — edit `funguy.conf` directly to change it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🕐 cron.py – Scheduled Commands *(Admin Only)*
|
||||||
|
|
||||||
|
In-process cron scheduler backed by APScheduler and SQLite (`cron_jobs.db`). Room context is automatically derived from where the command is issued — no room ID needed.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!cron add <cron_expr> <command> [tz=IANA]` | Schedule a command in the current room |
|
||||||
|
| `!cron remove <job_id>` | Delete a job |
|
||||||
|
| `!cron list` | List jobs in the current room |
|
||||||
|
| `!cron list *` | List all jobs across all rooms |
|
||||||
|
| `!cron enable <job_id>` | Re-enable a paused job |
|
||||||
|
| `!cron disable <job_id>` | Pause a job without deleting it |
|
||||||
|
| `!cron clear` | Remove all jobs from the current room |
|
||||||
|
|
||||||
|
**Cron expression format:** 5 fields — `minute hour day-of-month month day-of-week`
|
||||||
|
|
||||||
|
```
|
||||||
|
!cron add 0 8 * * * !weather London tz=Europe/London
|
||||||
|
```
|
||||||
|
|
||||||
|
Timezone defaults to UTC. Use any IANA zone name (e.g. `tz=America/New_York`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📅 date.py – Date & Time
|
||||||
|
|
||||||
|
```
|
||||||
|
!date
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays the current day of the week, ordinal date (e.g. "the 9th"), and 12-hour time. No API key required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔍 ddg.py – DuckDuckGo Search
|
||||||
|
|
||||||
|
No API key required. Uses the `ddgs` library.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!ddg <query>` | Top web result snippet (collapsible) |
|
||||||
|
| `!ddg search <query>` | 5 web results |
|
||||||
|
| `!ddg image <query>` | 3 image results |
|
||||||
|
| `!ddg news <query>` | 3 news articles |
|
||||||
|
| `!ddg video <query>` | 3 video results |
|
||||||
|
| `!ddg bang <!bang query>` | DuckDuckGo bang redirect |
|
||||||
|
| `!ddg define <word>` | Word definition |
|
||||||
|
| `!ddg calc <expression>` | Calculator |
|
||||||
|
| `!ddg weather [location]` | Weather snippet |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📖 dictionary.py – Word Definitions
|
||||||
|
|
||||||
|
```
|
||||||
|
!define <word>
|
||||||
|
```
|
||||||
|
|
||||||
|
Looks up definitions using the system `dict` command against WordNet (with fallback to all installed dictionaries). Requires the `dict` and `dict-wn` packages on the host (`sudo apt install dict dict-wn`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🌐 dns.py – DNS Reconnaissance *(SSRF-safe)*
|
||||||
|
|
||||||
|
```
|
||||||
|
!dns <domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
Queries A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, and PTR records and outputs a clean, emoji-aligned table. Private/internal IP targets are blocked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🗺️ dnsdumpster.py – DNSDumpster Recon
|
||||||
|
|
||||||
|
Requires `DNSDUMPSTER_KEY` in `.env`.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!dnsdumpster <domain>` | Full DNS mapping via DNSDumpster API |
|
||||||
|
| `!dnsdumpster test` | Test API connection against `google.com` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔧 encode.py – CyberChef-Style Toolkit
|
||||||
|
|
||||||
|
A fully offline, CyberChef-like data manipulation plugin with dozens of operations across encoding, cryptography, compression, data processing, forensics, and networking.
|
||||||
|
|
||||||
|
```
|
||||||
|
!encode <operation> [arguments] <data>
|
||||||
|
!encode help <op> # Detailed help for a specific operation
|
||||||
|
```
|
||||||
|
|
||||||
|
**Encoding**
|
||||||
|
|
||||||
|
| Operation | Example |
|
||||||
|
|-----------|---------|
|
||||||
|
| `base64 encode\|decode` | `!encode base64 encode Hello World` |
|
||||||
|
| `base32 encode\|decode` | `!encode base32 encode Hello` |
|
||||||
|
| `hex encode\|decode` | `!encode hex encode Secret` |
|
||||||
|
| `url encode\|decode` | `!encode url encode https://example.com/a b` |
|
||||||
|
| `html encode\|decode` | `!encode html encode "<script>"` |
|
||||||
|
| `unicode encode\|decode` | `!encode unicode encode café` |
|
||||||
|
| `binary encode\|decode` | `!encode binary encode Hi` |
|
||||||
|
| `rot13` | `!encode rot13 Uryyb Jbeyq` |
|
||||||
|
| `morse encode\|decode` | `!encode morse encode SOS` |
|
||||||
|
|
||||||
|
**Cryptography**
|
||||||
|
|
||||||
|
| Operation | Example |
|
||||||
|
|-----------|---------|
|
||||||
|
| `xor <key_hex>` | `!encode xor 41 Hello` |
|
||||||
|
| `aes encrypt\|decrypt <key_hex> <iv_hex>` | AES-CBC |
|
||||||
|
| `chacha20 encrypt\|decrypt <key_hex> <nonce_hex>` | ChaCha20 |
|
||||||
|
| `rsa encrypt\|decrypt <PEM_key>` | RSA-OAEP |
|
||||||
|
| `md5` / `sha1` / `sha256` / `sha512` | `!encode sha256 hello` |
|
||||||
|
| `sha3-256` / `sha3-512` | `!encode sha3-256 hello` |
|
||||||
|
| `hmac <algo> <key_hex>` | `!encode hmac sha256 6b6579 message` |
|
||||||
|
| `bcrypt hash <rounds>` / `bcrypt verify` | `!encode bcrypt hash 12 mypassword` |
|
||||||
|
| `argon2 hash <params>` / `argon2 verify` | PHC format |
|
||||||
|
| `pbkdf2 <salt_hex> <iters> <keylen> <algo>` | `!encode pbkdf2 aabbccdd 100000 32 sha256 pass` |
|
||||||
|
|
||||||
|
**Compression**
|
||||||
|
|
||||||
|
`gzip`, `zlib`, `bzip2`, `lzma` — each supports `compress` and `decompress` subcommands.
|
||||||
|
|
||||||
|
**Data Processing**
|
||||||
|
|
||||||
|
| Operation | Example |
|
||||||
|
|-----------|---------|
|
||||||
|
| `json format\|validate` | `!encode json format '{"key":"value"}'` |
|
||||||
|
| `xml format` | `!encode xml format "<root><a>1</a></root>"` |
|
||||||
|
| `yaml format\|tojson` | `!encode yaml format "key: value"` |
|
||||||
|
| `csv` | Parse CSV (first row as header) |
|
||||||
|
| `asn1` | Parse ASN.1 DER (base64 input) |
|
||||||
|
| `pemder topem\|toder` | PEM ↔ DER conversion |
|
||||||
|
|
||||||
|
**Forensics**
|
||||||
|
|
||||||
|
| Operation | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `entropy` | Shannon entropy of input |
|
||||||
|
| `ioc` | Extract IPs, domains, URLs, emails, hashes |
|
||||||
|
| `strings` | Extract ASCII strings (≥4 chars) from hex data |
|
||||||
|
| `filemagic` | Detect file type by magic bytes |
|
||||||
|
| `base64blob` | Find embedded Base64 blobs in text |
|
||||||
|
| `xbrute` | XOR single-byte brute force |
|
||||||
|
| `yara` | Scan with a YARA rule (rule as base64) |
|
||||||
|
| `peinfo` | PE header analysis (hex input) |
|
||||||
|
|
||||||
|
**Networking**
|
||||||
|
|
||||||
|
| Operation | Example |
|
||||||
|
|-----------|---------|
|
||||||
|
| `cidr` | `!encode cidr 192.168.1.0/24` |
|
||||||
|
| `ipconv hex\|dec\|binary` | `!encode ipconv hex 192.168.1.1` |
|
||||||
|
| `urlparse` | `!encode urlparse https://user:pass@example.com:8080/path?q=1` |
|
||||||
|
| `dns` | `!encode dns example.com` |
|
||||||
|
|
||||||
|
**Recipes** — chain multiple operations:
|
||||||
|
|
||||||
|
```
|
||||||
|
!encode recipe list
|
||||||
|
!encode recipe run '{"steps":[{"op":"base64","args":["encode"]},{"op":"hex","args":["encode"]}]}' "hello world"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💣 exploitdb.py – Exploit-DB Search
|
||||||
|
|
||||||
|
```
|
||||||
|
!exploitdb <search term> [max_results]
|
||||||
|
```
|
||||||
|
|
||||||
|
Searches the Exploit-DB CSV export from GitLab. Returns up to 10 results (default 5) with title, EDB-ID, type, platform, author, and a direct link. No API key required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🃏 fortune.py – Random Fortune
|
||||||
|
|
||||||
|
```
|
||||||
|
!fortune
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs `/usr/games/fortune` on the host and sends the result to the room. Requires the `fortune` package (`sudo apt install fortune`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📍 geo.py – IP / Domain Geolocation
|
||||||
|
|
||||||
|
```
|
||||||
|
!geo <ip_address or domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
Geolocates an IP or domain using `ip-api.com` (primary) with `ipapi.co` as fallback. Shows country, city, region, postal code, coordinates, timezone, ISP, organization, and ASN. Private IP targets are blocked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📰 hackernews.py – Hacker News
|
||||||
|
|
||||||
|
No API key required. Uses the official Firebase HN API and Algolia search.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!hn` | Top 5 stories (default) |
|
||||||
|
| `!hn top\|new\|best\|ask\|show\|job` | Stories by type |
|
||||||
|
| `!hn story <id>` | Full story details |
|
||||||
|
| `!hn comments <id>` | Top comments for a story |
|
||||||
|
| `!hn search <query>` | Full-text search via Algolia |
|
||||||
|
|
||||||
|
Append a number to set the result count: `!hn new 10`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔐 hashid.py – Hash Type Identifier
|
||||||
|
|
||||||
|
```
|
||||||
|
!hashid <hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
Identifies 100+ hash formats including MD5, SHA family, bcrypt, Argon2, yescrypt, scrypt, PBKDF2, NTLM, NetNTLMv2, LM, LDAP, Oracle, MSSQL, MySQL, phpBB3, WordPress, Drupal, and more. Displays Hashcat mode (`-m`) and John the Ripper format (`--format`) for each match, sorted by confidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔒 headers.py – HTTP Security Header Analysis
|
||||||
|
|
||||||
|
```
|
||||||
|
!headers <url>
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetches a URL and analyzes its HTTP security headers. Outputs a security score (0–100) plus a structured breakdown covering HSTS, CSP, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy, SSL certificate details, and actionable recommendations. Private/internal addresses are blocked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❓ help.py – Dynamic Help
|
||||||
|
|
||||||
|
```
|
||||||
|
!help
|
||||||
|
```
|
||||||
|
|
||||||
|
Aggregates and displays the `__help__` metadata from every currently loaded plugin in a single collapsible message.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎬 imdb.py – IMDb / OMDb Lookup
|
||||||
|
|
||||||
|
Requires `OMDB_API_KEY` in `.env`.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!imdb <title>` | Full details + poster image |
|
||||||
|
| `!imdb id <tt1234567>` | Lookup by IMDb ID |
|
||||||
|
| `!imdb search <query>` | Search titles |
|
||||||
|
| `!imdb episode <series> -s N -e N` | Episode info |
|
||||||
|
|
||||||
|
Optional flags: `-y <year>`, `-t movie|series|episode`, `--short-plot`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🤖 infermatic-text.py – AI Text Generation
|
||||||
|
|
||||||
|
Requires `INFERMATIC_API` (and optionally `INFERMATIC_MODEL`) in `.env`.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!text <prompt>` | Generate text with the default model |
|
||||||
|
| `!text --list-models` | List available models |
|
||||||
|
| `!text --use-model <model> <prompt>` | Use a specific model |
|
||||||
|
|
||||||
|
Optional flags: `--temperature <0.0–1.0>` (default 0.9), `--max-tokens <N>` (default 512)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🌐 isup.py – Site Availability Check
|
||||||
|
|
||||||
|
```
|
||||||
|
!isup <domain or IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
Performs DNS resolution then checks HTTP/HTTPS availability, reporting the status code and response time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 😂 joke.py – Random Jokes
|
||||||
|
|
||||||
|
```
|
||||||
|
!joke # General joke
|
||||||
|
!joke programming # Programming joke
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetches jokes from the Official Joke API. No API key required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⭐ karma.py – Room Karma Tracking
|
||||||
|
|
||||||
|
Karma is tracked by display name — Matrix IDs (`@user:server`) are **not** accepted.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!karma <user>` | Show karma points for a user |
|
||||||
|
| `!karma++ <user>` / `!-- <user>` | Give or remove karma |
|
||||||
|
| `!karma top [n]` / `!karma bottom [n]` | Leaderboard (top or bottom N) |
|
||||||
|
| `!karma rank <user>` | Show a user's rank |
|
||||||
|
| `!karma stats` | Room-wide statistics |
|
||||||
|
| `!karma history <user>` | Recent votes for a user |
|
||||||
|
|
||||||
|
Shortcuts: `!++ user`, `!-- user`, or inline `username++` / `username--` anywhere in a message.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎵 lastfm.py – Last.fm Music Stats
|
||||||
|
|
||||||
|
Requires `LASTFM_API_KEY` in `.env`. Run `!lastfm` for the full command list. Outputs neatly aligned code blocks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📡 news.py – News Headlines
|
||||||
|
|
||||||
|
Requires `GNEWS_API_KEY` in `.env`.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!news` | Top headlines |
|
||||||
|
| `!news top\|world\|tech\|business\|science\|health\|sports\|crypto` | Category headlines |
|
||||||
|
| `!news search <query>` | Search news |
|
||||||
|
|
||||||
|
Append a number to set result count: `!news tech 8`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔌 plugins.py – List Loaded Plugins
|
||||||
|
|
||||||
|
```
|
||||||
|
!plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays the total count of loaded plugins and a collapsible list with each plugin's name and description.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔗 proxy.py – SOCKS5 Proxy Finder
|
||||||
|
|
||||||
|
```
|
||||||
|
!proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetches a public proxy list, tests each candidate for connectivity, and returns a random working SOCKS5 proxy with its measured latency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💬 quote.py – Goodreads Quotes
|
||||||
|
|
||||||
|
```
|
||||||
|
!quote random
|
||||||
|
!quote <author>
|
||||||
|
```
|
||||||
|
|
||||||
|
Scrapes Goodreads using Playwright (headless Chromium). Requires `playwright`, `beautifulsoup4`, and `lxml` to be installed (see [Installation](#-installation)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 roomstats.py – Per-User Room Statistics
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!roomstats` | Aggregate room stats + top 10 users |
|
||||||
|
| `!rank <stat>` | Top 10 users by a specific statistic |
|
||||||
|
| `!stats [name]` | Show statistics for a specific user |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔎 shodan.py – Shodan Reconnaissance
|
||||||
|
|
||||||
|
Requires `SHODAN_KEY` in `.env`.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!shodan ip <ip>` | IP info with open ports and services |
|
||||||
|
| `!shodan search <query>` | Search internet-connected devices |
|
||||||
|
| `!shodan host <domain>` | Host and subdomain enumeration |
|
||||||
|
| `!shodan count <query>` | Result count for a query |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔐 sslscan.py – SSL/TLS Security Scanner
|
||||||
|
|
||||||
|
```
|
||||||
|
!sslscan <domain[:port]>
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests SSL/TLS protocol support, cipher suites, certificate validity, and known vulnerabilities. Provides a security score (0–100) and actionable recommendations. Defaults to port 443.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎨 stable-diffusion.py – Image Generation
|
||||||
|
|
||||||
|
Requires a locally running Stable Diffusion API (e.g. AUTOMATIC1111 or ComfyUI).
|
||||||
|
|
||||||
|
```
|
||||||
|
!sd [options] <prompt>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--steps N` | Sampling steps (default 4) |
|
||||||
|
| `--cfg <scale>` | CFG scale (default 2) |
|
||||||
|
| `--h H --w W` | Image height / width in pixels (default 512×512) |
|
||||||
|
| `--neg <prompt>` | Negative prompt |
|
||||||
|
| `--sampler <name>` | Sampler name (default `DPM++ SDE`) |
|
||||||
|
| `--seed <N>` | Deterministic seed |
|
||||||
|
|
||||||
|
LORAs can be embedded directly in the prompt: `<lora:filename:weight>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🌍 subdomains.py – Subdomain Enumeration
|
||||||
|
|
||||||
|
```
|
||||||
|
!subdomains <domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
Discovers subdomains using SSL certificate transparency logs via the CertSpotter API. No API key required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🧮 subnet.py – Subnet Calculator
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!subnet info <CIDR>` | Detailed network info (network, broadcast, hosts, mask, etc.) |
|
||||||
|
| `!subnet split <CIDR> --prefix <N>` | Split into smaller subnets by new prefix length |
|
||||||
|
| `!subnet split <CIDR> --diff <N>` | Split by prefix delta |
|
||||||
|
| `!subnet adjacent <CIDR> <count>` | Show current and adjacent networks |
|
||||||
|
|
||||||
|
Supports both IPv4 and IPv6. RFC 3021 `/31` and `/32` networks are handled correctly (both addresses listed as usable).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💻 sysinfo.py – System Information
|
||||||
|
|
||||||
|
```
|
||||||
|
!sysinfo
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays CPU usage and model, RAM, disk, network I/O, GPU info, temperature sensors, and top processes in a clean, emoji-aligned code block.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🕒 timezone.py – World Clock
|
||||||
|
|
||||||
|
```
|
||||||
|
!time <city> # Any city worldwide (geocoded)
|
||||||
|
!time <IANA zone> # e.g. Europe/London, Asia/Karachi
|
||||||
|
!time help # Show help
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples: `!time Lahore`, `!time New York`, `!time America/Chicago`
|
||||||
|
|
||||||
|
No city names are hardcoded. IANA zones resolve completely offline; city name lookup uses free geocoding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📚 urbandictionary.py – Urban Dictionary
|
||||||
|
|
||||||
|
```
|
||||||
|
!ud <term>
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetches top definitions from Urban Dictionary. No API key required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ☁️ weather.py – Current Weather
|
||||||
|
|
||||||
|
```
|
||||||
|
!weather <location>
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows temperature, feels-like, conditions, humidity, wind speed, and more in a clean aligned table. Uses OpenWeatherMap as the primary source (requires `OPENWEATHER_API_KEY`) with Open-Meteo as a free fallback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 👋 welcome.py – Room Welcome Message
|
||||||
|
|
||||||
|
```
|
||||||
|
!welcome
|
||||||
|
```
|
||||||
|
|
||||||
|
Manually triggers the room's configured welcome message for the requesting user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔍 whois.py – WHOIS Lookup
|
||||||
|
|
||||||
|
```
|
||||||
|
!whois <domain or IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns registrar, creation/expiry dates, nameservers, and registrant info in a clean aligned table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📖 wikipedia.py – Wikipedia Summary
|
||||||
|
|
||||||
|
```
|
||||||
|
!wp <search term>
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the lead section and main image of a Wikipedia article using the MediaWiki API. No scraping, no API key required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🗂️ xkcd.py – xkcd Comics
|
||||||
|
|
||||||
|
```
|
||||||
|
!xkcd # Random comic
|
||||||
|
!xkcd <number> # Specific comic (e.g. !xkcd 538)
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetches comics directly from the xkcd JSON API. No API key required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎥 youtube-search.py – YouTube Search
|
||||||
|
|
||||||
|
Requires `YOUTUBE_API_KEY` in `.env`.
|
||||||
|
|
||||||
|
```
|
||||||
|
!yt <search query>
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the top video results from YouTube with titles, channel names, and direct links.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Rate Limiting
|
||||||
|
|
||||||
|
Non-admin users are limited to **3 commands per 5 seconds**. Exceeding this limit returns:
|
||||||
|
|
||||||
|
> ⛔ You're sending commands too quickly. Please wait a few seconds.
|
||||||
|
|
||||||
|
The `admin_user` defined in `funguy.conf` is always exempt from rate limiting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security Notes
|
||||||
|
|
||||||
|
- All plugins that make outbound HTTP requests (`geo`, `dns`, `headers`, `sslscan`, etc.) validate that the target resolves to a **public IP address** before connecting. Private, loopback, link-local, and reserved ranges are blocked.
|
||||||
|
- The `admin_user` config field is **read-only via `!set`** — it can only be changed by editing `funguy.conf` directly, preventing privilege escalation through chat commands.
|
||||||
|
- Moderator commands (`!kick`, `!ban`, `!op`, etc.) require Matrix power level ≥ 50 in the room, independent of the global admin setting.
|
||||||
|
- The `encode.py` plugin operates entirely **offline** — no data is ever sent to external services.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Support
|
||||||
|
|
||||||
|
Join the community Matrix room for help, bug reports, and discussion:
|
||||||
|
|
||||||
|
**[Self-hosting | Security | Sysadmin | Homelab | Programming](https://matrix.to/#/#selfhosting:mozilla.org)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧙 Credits
|
||||||
|
|
||||||
|
**Creator & Developer:** HB ([@hashborgir:mozilla.org](https://matrix.to/#/@hashborgir:mozilla.org))
|
||||||
|
|
||||||
|
🍄 *Funguy Bot — built during recovery from cervical spinal surgery. Bugs are features in disguise.*
|
||||||
|
|||||||
+32
-34
@@ -37,13 +37,14 @@ from datetime import datetime, timedelta
|
|||||||
import re
|
import re
|
||||||
import asyncio
|
import asyncio
|
||||||
import traceback
|
import traceback
|
||||||
|
import time
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Configuration
|
# Configuration
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Prevent spam: minimum seconds between karma changes to same target by same user
|
# Global cooldown: one karma point per hour per voter
|
||||||
COOLDOWN_SECONDS = 5
|
COOLDOWN_SECONDS = 3600
|
||||||
|
|
||||||
# Database file
|
# Database file
|
||||||
DB_FILE = "karma.db"
|
DB_FILE = "karma.db"
|
||||||
@@ -55,7 +56,6 @@ display_name_cache = {}
|
|||||||
# Last time we refreshed the cache (per room)
|
# Last time we refreshed the cache (per room)
|
||||||
cache_timestamp = {}
|
cache_timestamp = {}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helper: pluralize "point" vs "points"
|
# Helper: pluralize "point" vs "points"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -130,37 +130,31 @@ async def refresh_display_name_cache(bot, room_id):
|
|||||||
logging.info(f"Refreshing display name cache for room {room_id}")
|
logging.info(f"Refreshing display name cache for room {room_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try to get room members from the bot's state
|
|
||||||
if hasattr(bot, 'async_client') and bot.async_client:
|
if hasattr(bot, 'async_client') and bot.async_client:
|
||||||
# Get the room state
|
resp = await bot.async_client.joined_members(room_id)
|
||||||
room = bot.async_client.rooms.get(room_id)
|
if resp.members is not None:
|
||||||
if room and hasattr(room, 'users'):
|
|
||||||
# Build mapping of display names to user IDs
|
|
||||||
name_map = {}
|
name_map = {}
|
||||||
for user_id, user_info in room.users.items():
|
for member in resp.members:
|
||||||
# Get display name - try different attributes
|
display_name = (member.display_name or "").strip()
|
||||||
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:
|
if display_name:
|
||||||
name_map[display_name.lower()] = user_id
|
name_map[display_name.lower()] = member.user_id
|
||||||
# Also store without emojis for easier matching
|
# Also store without emojis
|
||||||
clean_name = re.sub(r'[^\w\s]', '', display_name).strip().lower()
|
clean_name = re.sub(r'[^\w\s]', '', display_name).strip().lower()
|
||||||
if clean_name and clean_name != display_name.lower():
|
if clean_name and clean_name != display_name.lower():
|
||||||
name_map[clean_name] = user_id
|
name_map[clean_name] = member.user_id
|
||||||
|
|
||||||
display_name_cache[room_id] = name_map
|
display_name_cache[room_id] = name_map
|
||||||
cache_timestamp[room_id] = now
|
cache_timestamp[room_id] = now
|
||||||
logging.info(f"Cached {len(name_map)} display names for room {room_id}")
|
logging.info(f"Cached {len(name_map)} display names for room {room_id}")
|
||||||
|
# DEBUG: show first 5 names
|
||||||
|
sample = list(name_map.items())[:5]
|
||||||
|
logging.debug(f"Sample display names: {sample}")
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
logging.warning(f"joined_members returned None members for room {room_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"Could not refresh display name cache: {e}")
|
logging.warning(f"Could not refresh display name cache: {e}")
|
||||||
|
|
||||||
# If we couldn't get members, initialize empty cache
|
# init empty cache on failure
|
||||||
display_name_cache[room_id] = {}
|
display_name_cache[room_id] = {}
|
||||||
cache_timestamp[room_id] = now
|
cache_timestamp[room_id] = now
|
||||||
|
|
||||||
@@ -176,7 +170,10 @@ def resolve_display_name(room_id, display_name, bot=None):
|
|||||||
Returns None if the input is a Matrix ID (rejected) or if the name
|
Returns None if the input is a Matrix ID (rejected) or if the name
|
||||||
cannot be resolved.
|
cannot be resolved.
|
||||||
"""
|
"""
|
||||||
# Reject Matrix IDs outright
|
# Strip HTML tags (Matrix mention pills)
|
||||||
|
clean = re.sub(r'<[^>]+>', '', display_name).strip()
|
||||||
|
|
||||||
|
# Reject Matrix IDs outright (only if the raw input is an ID, not the cleaned one)
|
||||||
if is_matrix_id(display_name):
|
if is_matrix_id(display_name):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -184,21 +181,16 @@ def resolve_display_name(room_id, display_name, bot=None):
|
|||||||
if room_id in display_name_cache:
|
if room_id in display_name_cache:
|
||||||
name_map = display_name_cache[room_id]
|
name_map = display_name_cache[room_id]
|
||||||
|
|
||||||
# Try exact match (case-insensitive)
|
# Try exact match (case‑insensitive)
|
||||||
key = display_name.lower()
|
key = clean.lower()
|
||||||
if key in name_map:
|
if key in name_map:
|
||||||
return name_map[key]
|
return name_map[key]
|
||||||
|
|
||||||
# Try without emojis/special characters
|
# Try without emojis/special characters
|
||||||
clean_key = re.sub(r'[^\w\s]', '', display_name).strip().lower()
|
clean_key = re.sub(r'[^\w\s]', '', clean).strip().lower()
|
||||||
if clean_key and clean_key in name_map:
|
if clean_key and clean_key in name_map:
|
||||||
return name_map[clean_key]
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -567,7 +559,13 @@ async def process_karma_vote(room, display_name, action, voter, bot):
|
|||||||
# Check cooldown
|
# Check cooldown
|
||||||
if is_on_cooldown(room_id, user_id, voter_str):
|
if is_on_cooldown(room_id, user_id, voter_str):
|
||||||
remaining = get_cooldown_remaining(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.")
|
hours = remaining // 3600
|
||||||
|
minutes = (remaining % 3600) // 60
|
||||||
|
if hours > 0:
|
||||||
|
time_str = f"{hours}h {minutes}m"
|
||||||
|
else:
|
||||||
|
time_str = f"{minutes}m"
|
||||||
|
await bot.api.send_markdown_message(room.room_id, f"⏳ Slow down! You can give karma to that user again in {time_str}.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update karma
|
# Update karma
|
||||||
@@ -635,7 +633,7 @@ async def handle_karma_command(room, message, bot, config):
|
|||||||
<strong>Notes:</strong>
|
<strong>Notes:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
<li>You cannot modify your own karma</li>
|
<li>You cannot modify your own karma</li>
|
||||||
<li>There is a {COOLDOWN_SECONDS} second cooldown between votes</li>
|
<li>There is a 1‑hour cooldown per user you give karma to</li>
|
||||||
<li>Karma is tracked separately per room</li>
|
<li>Karma is tracked separately per room</li>
|
||||||
<li>Display names with emojis are supported</li>
|
<li>Display names with emojis are supported</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -784,7 +782,7 @@ async def handle_inline_karma(room, message, bot):
|
|||||||
if not matches:
|
if not matches:
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.info(f"Found inline karma matches: {matches}")
|
logging.debug(f"Found inline karma matches: {matches}")
|
||||||
|
|
||||||
responses = []
|
responses = []
|
||||||
for display_name, operator in matches:
|
for display_name, operator in matches:
|
||||||
|
|||||||
+5
-14
@@ -1,23 +1,13 @@
|
|||||||
|
simplematrixbotlib>=2.13.0
|
||||||
python-dotenv
|
python-dotenv
|
||||||
requests
|
aiohttp
|
||||||
nio
|
toml
|
||||||
markdown2
|
|
||||||
watchdog
|
|
||||||
emoji
|
|
||||||
python-slugify
|
|
||||||
youtube_title_parse
|
|
||||||
dnspython
|
dnspython
|
||||||
croniter
|
|
||||||
schedule
|
|
||||||
yt-dlp
|
|
||||||
pyopenssl
|
pyopenssl
|
||||||
psutil
|
psutil
|
||||||
toml
|
|
||||||
python-whois
|
python-whois
|
||||||
aiohttp
|
|
||||||
aiosqlite
|
aiosqlite
|
||||||
pillow
|
pillow
|
||||||
omdbapi
|
|
||||||
apscheduler
|
apscheduler
|
||||||
pytz
|
pytz
|
||||||
ddgs
|
ddgs
|
||||||
@@ -30,5 +20,6 @@ argon2-cffi
|
|||||||
yara-python
|
yara-python
|
||||||
asn1crypto
|
asn1crypto
|
||||||
PyYAML
|
PyYAML
|
||||||
lxml
|
|
||||||
wcwidth
|
wcwidth
|
||||||
|
markdown
|
||||||
|
python-cryptography-fernet-wrapper
|
||||||
|
|||||||
Reference in New Issue
Block a user