Updated karma 1 hour per target rate limit. Updated requirements.txt and README.md

This commit is contained in:
2026-05-09 23:16:19 -05:00
parent 733e1b43c5
commit a1ce95f72f
3 changed files with 827 additions and 190 deletions
+786 -138
View File
@@ -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
git clone https://gitlab.com/Eggzy/funguybot.git
cd funguybot
``` ```
MATRIX_URL="https://matrix.org" (or another homeserver)
MATRIX_USER=""
MATRIX_PASS=""
OPENWEATHER_API_KEY="" # Optional: For weather plugin
SMTP_SERVER = "example.com`" **2. Create and activate a Python virtual environment**
```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_PORT=465
SMTP_USER = "name@domain.tld" SMTP_USER=bot@example.com
SMTP_PASSWORD = "somepassword" SMTP_PASSWORD=yourpassword
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 ### Bot Configuration (`funguy.conf`)
Create `/etc/systemd/system/funguybot.service`
Replace `$working_directory` with your bot install path
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 (0100) 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.01.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 (0100) 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
View File
@@ -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 (caseinsensitive)
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 1hour 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
View File
@@ -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