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.
|
||||
- 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.
|
||||
## 📋 Table of Contents
|
||||
|
||||
## Automatic Installation
|
||||
Run the installation script
|
||||
1. `./install-funguy.sh`
|
||||
- [Features](#-features)
|
||||
- [Requirements](#-requirements)
|
||||
- [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
|
||||
`python3 -m venv venv`
|
||||
`source venv/bin/activate`
|
||||
- **Modular plugin system** – each command lives in its own `plugins/*.py` file; add or remove features without touching core code
|
||||
- **Runtime plugin management** – load, unload, enable, or disable plugins per-room with no restart required
|
||||
- **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
|
||||
`cp api.py.patch simplematrixbotlib`
|
||||
`git apply api.py.patch`
|
||||
## 📦 Requirements
|
||||
|
||||
4. Install dependencies:
|
||||
`cd simplematrixbotlib && pip install .`
|
||||
`cd ../ && pip install -r requirements.txt`
|
||||
- Python 3.9+
|
||||
- A Matrix homeserver account for the bot
|
||||
- `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`
|
||||
`playwright install chromium`
|
||||
## 🚀 Installation
|
||||
|
||||
6. Set up environment variables:
|
||||
Create/Edit `.env` file in the root directory of the bot and add the following variables:
|
||||
**1. Clone the repository**
|
||||
|
||||
```
|
||||
MATRIX_URL="https://matrix.org" (or another homeserver)
|
||||
MATRIX_USER=""
|
||||
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=
|
||||
```bash
|
||||
git clone https://gitlab.com/Eggzy/funguybot.git
|
||||
cd funguybot
|
||||
```
|
||||
|
||||
7. Create systemd.service
|
||||
Create `/etc/systemd/system/funguybot.service`
|
||||
Replace `$working_directory` with your bot install path
|
||||
**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_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]
|
||||
Description=Funguy Bot Service
|
||||
After=network.target
|
||||
@@ -88,101 +159,678 @@ SyslogIdentifier=funguybot
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
```
|
||||
|
||||
8. Launch Fungy
|
||||
```
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable funguybot
|
||||
systemctl start funguybot
|
||||
```
|
||||
|
||||
# 🍄 Funguy Bot Commands 🍄
|
||||
### Direct Launch
|
||||
|
||||
## Available Plugins
|
||||
|
||||
The bot includes the following plugins:
|
||||
|
||||
- **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
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
python3 funguy.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*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 asyncio
|
||||
import traceback
|
||||
import time
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Prevent spam: minimum seconds between karma changes to same target by same user
|
||||
COOLDOWN_SECONDS = 5
|
||||
# Global cooldown: one karma point per hour per voter
|
||||
COOLDOWN_SECONDS = 3600
|
||||
|
||||
# Database file
|
||||
DB_FILE = "karma.db"
|
||||
@@ -55,7 +56,6 @@ display_name_cache = {}
|
||||
# Last time we refreshed the cache (per room)
|
||||
cache_timestamp = {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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}")
|
||||
|
||||
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
|
||||
resp = await bot.async_client.joined_members(room_id)
|
||||
if resp.members is not None:
|
||||
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
|
||||
|
||||
for member in resp.members:
|
||||
display_name = (member.display_name or "").strip()
|
||||
if display_name:
|
||||
name_map[display_name.lower()] = user_id
|
||||
# Also store without emojis for easier matching
|
||||
name_map[display_name.lower()] = member.user_id
|
||||
# Also store without emojis
|
||||
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
|
||||
|
||||
name_map[clean_name] = member.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}")
|
||||
# DEBUG: show first 5 names
|
||||
sample = list(name_map.items())[:5]
|
||||
logging.debug(f"Sample display names: {sample}")
|
||||
return
|
||||
|
||||
else:
|
||||
logging.warning(f"joined_members returned None members for room {room_id}")
|
||||
except Exception as 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] = {}
|
||||
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
|
||||
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):
|
||||
return None
|
||||
|
||||
@@ -184,21 +181,16 @@ def resolve_display_name(room_id, display_name, bot=None):
|
||||
if room_id in display_name_cache:
|
||||
name_map = display_name_cache[room_id]
|
||||
|
||||
# Try exact match (case-insensitive)
|
||||
key = display_name.lower()
|
||||
# Try exact match (case‑insensitive)
|
||||
key = clean.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()
|
||||
clean_key = re.sub(r'[^\w\s]', '', clean).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
|
||||
|
||||
|
||||
@@ -567,7 +559,13 @@ async def process_karma_vote(room, display_name, action, voter, bot):
|
||||
# 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.")
|
||||
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
|
||||
|
||||
# Update karma
|
||||
@@ -635,7 +633,7 @@ async def handle_karma_command(room, message, bot, config):
|
||||
<strong>Notes:</strong>
|
||||
<ul>
|
||||
<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>Display names with emojis are supported</li>
|
||||
</ul>
|
||||
@@ -784,7 +782,7 @@ async def handle_inline_karma(room, message, bot):
|
||||
if not matches:
|
||||
return
|
||||
|
||||
logging.info(f"Found inline karma matches: {matches}")
|
||||
logging.debug(f"Found inline karma matches: {matches}")
|
||||
|
||||
responses = []
|
||||
for display_name, operator in matches:
|
||||
|
||||
+6
-15
@@ -1,23 +1,13 @@
|
||||
simplematrixbotlib>=2.13.0
|
||||
python-dotenv
|
||||
requests
|
||||
nio
|
||||
markdown2
|
||||
watchdog
|
||||
emoji
|
||||
python-slugify
|
||||
youtube_title_parse
|
||||
aiohttp
|
||||
toml
|
||||
dnspython
|
||||
croniter
|
||||
schedule
|
||||
yt-dlp
|
||||
pyopenssl
|
||||
psutil
|
||||
toml
|
||||
python-whois
|
||||
aiohttp
|
||||
aiosqlite
|
||||
pillow
|
||||
omdbapi
|
||||
apscheduler
|
||||
pytz
|
||||
ddgs
|
||||
@@ -30,5 +20,6 @@ argon2-cffi
|
||||
yara-python
|
||||
asn1crypto
|
||||
PyYAML
|
||||
lxml
|
||||
wcwidth
|
||||
wcwidth
|
||||
markdown
|
||||
python-cryptography-fernet-wrapper
|
||||
|
||||
Reference in New Issue
Block a user