Compare commits
28 Commits
c4b6c750fb
...
main
Author | SHA1 | Date | |
---|---|---|---|
d61036d5ac | |||
9da1009c0e | |||
4306d013eb | |||
bb6f6c15f6 | |||
428d21d884 | |||
5ace1083f1 | |||
8eb21d49da | |||
b63cccca7a | |||
501df0ad3d | |||
4efb3e745e | |||
25d84ed392 | |||
89fc69557a | |||
5a320014b9 | |||
dbd93583c1 | |||
2d4ff9c1e2 | |||
712715b174 | |||
6f923e79bd | |||
e575598772 | |||
2feeb339f2 | |||
5d746027e2 | |||
d2acef611b | |||
e8186c9fec | |||
543e139ca0 | |||
551c5ddc02 | |||
387606426c | |||
41c59618cc | |||
5d6ad98303 | |||
4ef6f3beb7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ chromedriver
|
|||||||
store
|
store
|
||||||
funguybot.service
|
funguybot.service
|
||||||
stats.db
|
stats.db
|
||||||
|
cron.db
|
||||||
|
410
README.md
410
README.md
@@ -25,7 +25,7 @@ Run the installation script
|
|||||||
`source venv/bin/activate`
|
`source venv/bin/activate`
|
||||||
|
|
||||||
2. Clone the repository:
|
2. Clone the repository:
|
||||||
`git clone https://git.stoned.io/hash/FunguyBot`
|
`git clone https://gitlab.com/Eggzy/funguybot.git`
|
||||||
|
|
||||||
3. Apply the patch
|
3. Apply the patch
|
||||||
`cp api.py.patch simplematrixbotlib`
|
`cp api.py.patch simplematrixbotlib`
|
||||||
@@ -42,6 +42,7 @@ Create/Edit `.env` file in the root directory of the bot and add the following v
|
|||||||
MATRIX_URL="https://matrix.org" (or another homeserver)
|
MATRIX_URL="https://matrix.org" (or another homeserver)
|
||||||
MATRIX_USER=""
|
MATRIX_USER=""
|
||||||
MATRIX_PASS=""
|
MATRIX_PASS=""
|
||||||
|
OPENWEATHER_API_KEY="" # Optional: For weather plugin
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create systemd.service
|
4. Create systemd.service
|
||||||
@@ -88,50 +89,395 @@ To use the bot, invite it to a Matrix room and interact with it by sending comma
|
|||||||
- `!funguy <prompt>` Talk to the Tech AI LLM
|
- `!funguy <prompt>` Talk to the Tech AI LLM
|
||||||
- `!music <prompt>` Talk to the music knowledge LLM
|
- `!music <prompt>` Talk to the music knowledge LLM
|
||||||
- `!yt <search terms>` Search Youtube
|
- `!yt <search terms>` Search Youtube
|
||||||
|
- `!weather New York` Get Weather information
|
||||||
|
- `!ud <term>` Get Urban Dictionary definition
|
||||||
|
- `!help` Get Help
|
||||||
For a complete list of available commands and their descriptions, use the `!commands` command.
|
For a complete list of available commands and their descriptions, use the `!commands` command.
|
||||||
|
|
||||||
# 🍄 Funguy Bot Commands 🍄
|
# 🍄 Funguy Bot Commands 🍄
|
||||||
|
|
||||||
🃏 **!fortune**
|
|
||||||
Returns a random fortune message.
|
|
||||||
Executes the `/usr/games/fortune` utility and sends the output as a message to the chat room.
|
|
||||||
|
|
||||||
⏰ **!date**
|
## Plugin Documentation
|
||||||
Displays the current date and time.
|
|
||||||
Fetches the current date and time using Python's `datetime` module and sends it in a formatted message to the chat room.
|
|
||||||
|
|
||||||
💻 **!proxy**
|
### Core Commands
|
||||||
Retrieves a tested/working random SOCKS5 proxy.
|
|
||||||
Fetches a list of SOCKS5 proxies, tests their availability, and sends the first working proxy to the chat room.
|
|
||||||
|
|
||||||
📶 **!isup <domain/ip>**
|
**🍄 !help**
|
||||||
Checks if the specified domain or IP address is reachable.
|
Displays comprehensive help documentation for all available commands with usage examples.
|
||||||
Checks if the specified domain or IP address is reachable by attempting to ping it. If DNS resolution is successful, it checks HTTP and HTTPS service availability by sending requests to the domain.
|
|
||||||
|
|
||||||
☯ **!karma <user>**
|
**🔌 !plugins**
|
||||||
Retrieves the karma points for the specified user.
|
Lists all loaded plugins along with their descriptions.
|
||||||
Retrieves the karma points for the specified user from a database and sends them as a message to the chat room.
|
|
||||||
|
|
||||||
⇧ **!karma user up**
|
**⏰ !date**
|
||||||
Increases the karma points for the specified user by 1.
|
Displays the current date and time with proper ordinal formatting.
|
||||||
Increases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room.
|
|
||||||
|
|
||||||
⇩ **!karma user down**
|
**🃏 !fortune**
|
||||||
Decreases the karma points for the specified user by 1.
|
Returns a random fortune message using the fortune command.
|
||||||
Decreases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room.
|
|
||||||
|
|
||||||
📄 **!funguy [prompt]**
|
### Utility Commands
|
||||||
An AI large language model designed to serve as a chatbot within a vibrant and diverse community chat room hosted on the Matrix platform.
|
|
||||||
(Currently loaded model: **TheBloke_Mistral-7B-Instruct-v0.2-GPTQ**
|
|
||||||
|
|
||||||
🎝 **!music [prompt]**
|
**💻 !proxy**
|
||||||
Your music expert! Try it out.
|
Retrieves and tests random SOCKS5 proxies from public sources, showing latency and caching working proxies.
|
||||||
|
|
||||||
# 🌟 Funguy Bot Credits 🌟
|
**📶 !isup [domain/ip]**
|
||||||
|
Checks if a website or server is reachable, including DNS resolution and HTTP/HTTPS service checks.
|
||||||
|
|
||||||
🧙♂️ Creator & Developer:
|
**☯ !karma [user] [up/down]**
|
||||||
- Hash Borgir is the author of 🍄Funguy Bot🍄. (@hashborgir:mozilla.org)
|
Manages karma points for users. View karma with `!karma user`, increase with `!karma user up`, decrease with `!karma user down`.
|
||||||
|
|
||||||
# Join our Matrix Room:
|
**🌧️ !weather [location]**
|
||||||
|
Fetches current weather information for any location using OpenWeatherMap API.
|
||||||
|
*Requires OPENWEATHER_API_KEY environment variable*
|
||||||
|
|
||||||
[Self-hosting | Security | Sysadmin | Homelab | Programming](https://chat.mozilla.org/#/room/#selfhosting:mozilla.org)
|
**📖 !ud [term] [index]**
|
||||||
|
Fetches definitions from Urban Dictionary. Use without arguments for random definition, or specify term and optional index.
|
||||||
|
|
||||||
|
**🔍 !dns [domain]**
|
||||||
|
Performs comprehensive DNS reconnaissance on a domain. Shows A, AAAA, MX, NS, TXT, CNAME, SOA, and other DNS records.
|
||||||
|
|
||||||
|
**💰 !btc**
|
||||||
|
Fetches the current Bitcoin price in USD from bitcointicker.co API.
|
||||||
|
|
||||||
|
### 🔍 Shodan Security Research
|
||||||
|
|
||||||
|
**📡 !shodan [command] [query]**
|
||||||
|
Shodan.io integration for security reconnaissance and threat intelligence.
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `!shodan ip <ip_address>` - Detailed IP information (services, ports, banners)
|
||||||
|
- `!shodan search <query>` - Search Shodan database with filters
|
||||||
|
- `!shodan host <domain>` - Host information and subdomain enumeration
|
||||||
|
- `!shodan count <query>` - Count results with geographic/organization breakdown
|
||||||
|
- `!shodan test` - Test API connection and debug queries
|
||||||
|
|
||||||
|
**Search Examples:**
|
||||||
|
```bash
|
||||||
|
!shodan search apache
|
||||||
|
!shodan search "port:22 country:US"
|
||||||
|
!shodan search "product:nginx city:'New York'"
|
||||||
|
!shodan search "net:192.168.1.0/24"
|
||||||
|
!shodan search "vuln:cve-2021-44228"
|
||||||
|
!shodan search "http.title:'phpMyAdmin'"
|
||||||
|
!shodan search "ssl.cert.subject.cn:'example.com'"
|
||||||
|
|
||||||
|
Common Search Filters:
|
||||||
|
country:US - Filter by country
|
||||||
|
city:"New York" - Filter by city
|
||||||
|
port:80,443,8080 - Filter by ports
|
||||||
|
product:nginx - Filter by service/product
|
||||||
|
os:Windows - Filter by operating system
|
||||||
|
org:"Google" - Filter by organization
|
||||||
|
net:192.168.0.0/16 - Filter by network range
|
||||||
|
has_ssl:true - Has SSL certificate
|
||||||
|
http.title:"admin" - HTTP page title contains
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔍 DNSDumpster Reconnaissance
|
||||||
|
|
||||||
|
**🌐 !dnsdumpster [domain]**
|
||||||
|
Comprehensive DNS reconnaissance and attack surface mapping using DNSDumpster.com API.
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `!dnsdumpster <domain>` - Complete DNS reconnaissance for any domain
|
||||||
|
- `!dnsdumpster test` - Test API connection and key validity
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- **A Records**: All IPv4 addresses with geographic and ASN information
|
||||||
|
- **NS Records**: Complete name server information with IP locations
|
||||||
|
- **MX Records**: All mail servers with geographic data
|
||||||
|
- **CNAME Records**: Full alias chain mappings
|
||||||
|
- **TXT Records**: All text records including SPF, DKIM, verification records
|
||||||
|
- **Additional Records**: AAAA, SRV, SOA, PTR records when available
|
||||||
|
- **Web Services**: HTTP/HTTPS service detection with banner information
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
!dnsdumpster google.com
|
||||||
|
!dnsdumpster github.com
|
||||||
|
!dnsdumpster example.com
|
||||||
|
!dnsdumpster test
|
||||||
|
|
||||||
|
Data Returned:
|
||||||
|
Total record counts for each type
|
||||||
|
IP addresses with country and ASN information
|
||||||
|
Web server banners and technologies
|
||||||
|
Complete subdomain and host mappings
|
||||||
|
Geographic distribution of services
|
||||||
|
Requires DNSDUMPSTER_KEY environment variable in .env file
|
||||||
|
```
|
||||||
|
|
||||||
|
## ExploitDB Plugin
|
||||||
|
|
||||||
|
A security plugin that searches Exploit-DB for vulnerabilities and exploits directly from Matrix.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Searches the official Exploit-DB CSV database for security exploits
|
||||||
|
- Provides direct links to exploit details
|
||||||
|
- Fallback to web search when CSV lookup fails
|
||||||
|
- Configurable result limits (1-10)
|
||||||
|
- Formatted output with exploit metadata
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
- `!exploitdb <search term> [max_results]` - Search Exploit-DB for vulnerabilities
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
!exploitdb wordpress
|
||||||
|
!exploitdb apache 3
|
||||||
|
!exploitdb windows privilege escalation
|
||||||
|
!exploitdb android 10
|
||||||
|
```
|
||||||
|
### Usage Notes
|
||||||
|
- Maximum results limited to 10 for performance
|
||||||
|
- Results include: title, EDB-ID, type, platform, author, date, and direct URL
|
||||||
|
- Includes responsible disclosure reminder
|
||||||
|
- Automatically falls back to search links if CSV database is unavailable
|
||||||
|
|
||||||
|
|
||||||
|
### 🔒 HTTP Security Headers Analysis
|
||||||
|
|
||||||
|
**🛡️ !headers [url]**
|
||||||
|
Comprehensive HTTP security header analysis with security scoring and recommendations.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- **Security Scoring**: 0-100 rating based on headers configuration
|
||||||
|
- **Header Validation**: Checks presence and proper configuration of critical security headers
|
||||||
|
- **Redirect Analysis**: Follows HTTP to HTTPS redirect chain
|
||||||
|
- **SSL Certificate**: Basic SSL/TLS certificate information
|
||||||
|
- **Information Disclosure**: Identifies revealing server headers
|
||||||
|
- **Actionable Recommendations**: Specific guidance for security improvements
|
||||||
|
|
||||||
|
**Security Headers Analyzed:**
|
||||||
|
- `Strict-Transport-Security` (HSTS) - HTTP to HTTPS enforcement
|
||||||
|
- `Content-Security-Policy` (CSP) - XSS and content injection protection
|
||||||
|
- `X-Frame-Options` - Clickjacking protection
|
||||||
|
- `X-Content-Type-Options` - MIME type sniffing prevention
|
||||||
|
- `Referrer-Policy` - Referrer information control
|
||||||
|
- `Feature-Policy` / `Permissions-Policy` - Browser feature restrictions
|
||||||
|
- Information disclosure headers (`Server`, `X-Powered-By`)
|
||||||
|
|
||||||
|
**Security Ratings:**
|
||||||
|
- **🟢 Excellent (80-100)**: Strong security headers configuration
|
||||||
|
- **🟡 Good (60-79)**: Moderate security, room for improvement
|
||||||
|
- **🟠 Fair (40-59)**: Basic security, significant improvements needed
|
||||||
|
- **🔴 Poor (0-39)**: Weak security headers configuration
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
!headers example.com
|
||||||
|
!headers https://github.com
|
||||||
|
!headers localhost:8080
|
||||||
|
!headers subdomain.target.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔐 Hash Identification
|
||||||
|
|
||||||
|
**🔄 !hashid [hash]**
|
||||||
|
Advanced hash type identification with confidence scoring and tool recommendations.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- **Comprehensive Detection**: 100+ hash types including modern, legacy, and exotic algorithms
|
||||||
|
- **Confidence Scoring**: Color-coded confidence levels (🟢 Very High to 🔴 Low)
|
||||||
|
- **Tool Integration**: Hashcat mode numbers and John the Ripper format names
|
||||||
|
- **Context-Aware**: Handles modular crypt formats, LDAP, database, and network hashes
|
||||||
|
|
||||||
|
**Supported Hash Categories:**
|
||||||
|
- **Modern Algorithms**: yescrypt, scrypt, Argon2 (i/d/id), bcrypt variants
|
||||||
|
- **Unix/Linux**: SHA-512/256 Crypt, MD5 Crypt, Apache MD5 (apr1)
|
||||||
|
- **Raw Hashes**: MD5, SHA-1/224/256/384/512, SHA-3, Keccak, BLAKE2
|
||||||
|
- **Windows**: NTLM, LM, NetNTLMv1/v2
|
||||||
|
- **Databases**: MySQL (4.1+, old), PostgreSQL, Oracle (11g, 12c), MSSQL
|
||||||
|
- **Web/CMS**: WordPress, phpBB3, Drupal 7+, Django PBKDF2
|
||||||
|
- **LDAP**: SSHA, SMD5, various LDAP crypt formats
|
||||||
|
- **Exotic**: Whirlpool, RIPEMD, GOST, Tiger, Haval
|
||||||
|
|
||||||
|
**Tool Integration:**
|
||||||
|
- **Hashcat**: Mode numbers for direct use with `-m` parameter
|
||||||
|
- **John the Ripper**: Format names for `--format=` parameter
|
||||||
|
- **Multi-tool Support**: Works with most popular password cracking tools
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
!hashid 5d41402abc4b2a76b9719d911017c592
|
||||||
|
!hashid aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
|
||||||
|
!hashid $6$rounds=5000$salt$hashvalue...
|
||||||
|
!hashid $y$j9T$... (modern Linux yescrypt)
|
||||||
|
!hashid 8846f7eaee8fb117ad06bdd830b7586c
|
||||||
|
!hashid *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔐 SSL/TLS Security Scanner
|
||||||
|
|
||||||
|
**🔐 !sslscan [domain[:port]]**
|
||||||
|
Comprehensive SSL/TLS security scanning and analysis with vulnerability detection.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- **Protocol Analysis**: TLS 1.0-1.3 support testing with security scoring
|
||||||
|
- **Certificate Validation**: Chain validation, expiration, signature algorithms
|
||||||
|
- **Cipher Suite Testing**: 25+ cipher suites with strength classification
|
||||||
|
- **Vulnerability Detection**: POODLE, weak ciphers, protocol vulnerabilities
|
||||||
|
- **Security Scoring**: 0-100 rating with color-coded assessment
|
||||||
|
- **Compliance Checking**: PCI DSS and modern security standards
|
||||||
|
|
||||||
|
**Security Checks:**
|
||||||
|
- **Protocol Security**: TLS 1.2/1.3 enforcement, insecure protocol detection
|
||||||
|
- **Certificate Health**: Expiration monitoring, signature strength validation
|
||||||
|
- **Cipher Security**: RC4, DES, 3DES, NULL cipher detection and classification
|
||||||
|
- **Modern Standards**: Forward Secrecy, strong encryption, best practices
|
||||||
|
|
||||||
|
**Output Features:**
|
||||||
|
- **Security Score**: Overall rating (🟢 Excellent to 🔴 Poor)
|
||||||
|
- **Detailed Breakdown**: Protocol support, cipher analysis, certificate info
|
||||||
|
- **Vulnerability List**: CVE references and severity ratings
|
||||||
|
- **Actionable Recommendations**: Specific fixes and configuration improvements
|
||||||
|
- **Quick Assessment**: Executive summary for rapid evaluation
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
!sslscan example.com
|
||||||
|
!sslscan github.com:443
|
||||||
|
!sslscan localhost:8443
|
||||||
|
!sslscan 192.168.1.1:443
|
||||||
|
```
|
||||||
|
🟢 Excellent (90-100): Modern TLS configuration with strong security
|
||||||
|
🟡 Good (80-89): Good security with minor improvements needed
|
||||||
|
🟠 Fair (60-79): Moderate security, significant improvements recommended
|
||||||
|
🔴 Poor (0-59): Critical security issues requiring immediate attention
|
||||||
|
|
||||||
|
*Note: SSLv2/SSLv3 testing limited by Python security features (intentional security measure)*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### AI & Generation Commands
|
||||||
|
|
||||||
|
**🤖 AI Commands (!tech, !music, !eth, etc.)**
|
||||||
|
Multiple AI model commands that interface with local AI API. Each command uses specialized prompts for different domains:
|
||||||
|
- `!tech` - Technology assistance
|
||||||
|
- `!music` - Music knowledge and recommendations
|
||||||
|
- `!weather` - Weather information
|
||||||
|
- And 100+ other specialized AI commands
|
||||||
|
|
||||||
|
**📸 !sd [prompt] [options]**
|
||||||
|
Generates images using self-hosted Stable Diffusion with customizable parameters:
|
||||||
|
- `--steps` - Number of generation steps (default: 4)
|
||||||
|
- `--cfg` - CFG scale (default: 2)
|
||||||
|
- `--h` - Image height (default: 512)
|
||||||
|
- `--w` - Image width (default: 512)
|
||||||
|
- `--neg` - Negative prompt
|
||||||
|
- `--sampler` - Sampler name (default: DPM++ SDE)
|
||||||
|
|
||||||
|
**📄 !text [prompt] [options]**
|
||||||
|
Generates text using Ollama's Mistral 7B Instruct model:
|
||||||
|
- `--max_tokens` - Maximum tokens to generate (default: 512)
|
||||||
|
- `--temperature` - Sampling temperature (default: 0.7)
|
||||||
|
|
||||||
|
### Media & Search Commands
|
||||||
|
|
||||||
|
**🎬 YouTube Commands**
|
||||||
|
- Automatic preview when YouTube links are posted
|
||||||
|
- `!yt [search terms]` - Search for YouTube videos
|
||||||
|
- Shows video info, description, and attempts to fetch lyrics
|
||||||
|
|
||||||
|
**📰 !xkcd**
|
||||||
|
Fetches and displays a random XKCD comic.
|
||||||
|
|
||||||
|
### Administration Commands
|
||||||
|
*Admin only - requires admin_user configuration*
|
||||||
|
|
||||||
|
**🔧 !set [option] [value]**
|
||||||
|
Set configuration options (admin_user, prefix)
|
||||||
|
|
||||||
|
**🔍 !get [option]**
|
||||||
|
Get configuration values
|
||||||
|
|
||||||
|
**💾 !saveconf**
|
||||||
|
Save current configuration
|
||||||
|
|
||||||
|
**📥 !loadconf**
|
||||||
|
Load saved configuration
|
||||||
|
|
||||||
|
**👁️ !show**
|
||||||
|
Display current configuration
|
||||||
|
|
||||||
|
**🔄 !reset**
|
||||||
|
Reset configuration to defaults
|
||||||
|
|
||||||
|
**📤 !load [plugin]**
|
||||||
|
Load a plugin
|
||||||
|
|
||||||
|
**📥 !unload [plugin]**
|
||||||
|
Unload a plugin
|
||||||
|
|
||||||
|
**🔄 !reload**
|
||||||
|
Reload all plugins
|
||||||
|
|
||||||
|
**🚫 !disable [plugin] [room_id]**
|
||||||
|
Disable a plugin for specific room
|
||||||
|
|
||||||
|
**✅ !enable [plugin] [room_id]**
|
||||||
|
Enable a plugin for specific room
|
||||||
|
|
||||||
|
**⚙️ !rehash**
|
||||||
|
Reload configuration
|
||||||
|
|
||||||
|
### Cron System
|
||||||
|
|
||||||
|
**⏱️ !cron [add|remove] [room_id] [cron_entry] [command]**
|
||||||
|
Schedule automated commands using cron syntax:
|
||||||
|
- `add` - Add a new cron job
|
||||||
|
- `remove` - Remove an existing cron job
|
||||||
|
|
||||||
|
## Full AI Command List
|
||||||
|
|
||||||
|
The bot includes over 100 specialized AI commands covering various domains:
|
||||||
|
|
||||||
|
**Creative & Writing**: !write, !script, !author, !poem, !rap, !story, !comic, !motiv, !debate
|
||||||
|
|
||||||
|
**Technical**: !tech, !dev, !py, !php, !regex, !math, !web, !it, !security, !ai, !ml, !data, !game
|
||||||
|
|
||||||
|
**Professional**: !seo, !recruit, !coach, !devrel, !sales, !ceo, !mgmt, !startup, !invest, !fin
|
||||||
|
|
||||||
|
**Educational**: !tutor, !teach, !edu, !acad, !hist, !astro, !chem, !math, !psych
|
||||||
|
|
||||||
|
**Lifestyle**: !fit, !health, !diet, !cook, !travel, !art, !music, !film, !gaming
|
||||||
|
|
||||||
|
**Specialized**: !legal, !medical, !realest, !auto, !fashion, !design, !interior
|
||||||
|
|
||||||
|
And many more! Use `!help` in chat to see the complete list with descriptions.
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- 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!*
|
||||||
|
@@ -10,3 +10,8 @@ blocklist = []
|
|||||||
admin_user = "@hashborgir:mozilla.org"
|
admin_user = "@hashborgir:mozilla.org"
|
||||||
prefix = "!"
|
prefix = "!"
|
||||||
config_file = "funguy.conf"
|
config_file = "funguy.conf"
|
||||||
|
|
||||||
|
[plugins.disabled]
|
||||||
|
"!uFhErnfpYhhlauJsNK:matrix.org" = [ "youtube-preview", "ai", "proxy",]
|
||||||
|
"!vYcfWXpPvxeQvhlFdV:matrix.org" = []
|
||||||
|
"!NXdVjDXPxXowPkrJJY:matrix.org" = [ "karma",]
|
||||||
|
263
funguy.py
263
funguy.py
@@ -1,95 +1,256 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# funguy.py
|
|
||||||
|
|
||||||
import os
|
"""
|
||||||
import logging
|
Funguy Bot Class
|
||||||
import importlib
|
"""
|
||||||
import simplematrixbotlib as botlib
|
|
||||||
from dotenv import load_dotenv
|
# Importing necessary libraries and modules
|
||||||
import time
|
import os # Operating System functions
|
||||||
import sys
|
import logging # Logging library for logging messages
|
||||||
|
import importlib # Library for dynamically importing modules
|
||||||
|
import simplematrixbotlib as botlib # Library for interacting with Matrix chat
|
||||||
|
from dotenv import load_dotenv # Library for loading environment variables from a .env file
|
||||||
|
import time # Time-related functions
|
||||||
|
import sys # System-specific parameters and functions
|
||||||
|
import toml # Library for parsing TOML configuration files
|
||||||
|
|
||||||
|
# Importing FunguyConfig class from plugins.config module
|
||||||
from plugins.config import FunguyConfig
|
from plugins.config import FunguyConfig
|
||||||
|
|
||||||
class FunguyBot:
|
# Whitelist of allowed plugins to prevent arbitrary code execution
|
||||||
|
ALLOWED_PLUGINS = {
|
||||||
|
'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma',
|
||||||
|
'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion',
|
||||||
|
'xkcd', 'youtube-preview', 'youtube-search', 'weather', 'urbandictionary',
|
||||||
|
'bitcoin', 'dns', 'shodan', 'dnsdumpster', 'exploitdb', 'headers', 'hashid',
|
||||||
|
'sslscan'
|
||||||
|
}
|
||||||
|
|
||||||
bot = None
|
class FunguyBot:
|
||||||
config = None
|
"""
|
||||||
|
A bot class for managing plugins and handling commands in a Matrix chat environment.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- __init__: Constructor method for initializing the bot.
|
||||||
|
- load_dotenv: Method to load environment variables from a .env file.
|
||||||
|
- setup_logging: Method to configure logging settings.
|
||||||
|
- load_plugins: Method to load plugins from the specified directory.
|
||||||
|
- reload_plugins: Method to reload all plugins.
|
||||||
|
- load_config: Method to load configuration settings.
|
||||||
|
- load_disabled_plugins: Method to load disabled plugins from configuration file.
|
||||||
|
- save_disabled_plugins: Method to save disabled plugins to configuration file.
|
||||||
|
- handle_commands: Method to handle incoming commands and dispatch them to appropriate plugins.
|
||||||
|
- rehash_config: Method to rehash the configuration settings.
|
||||||
|
- disable_plugin: Method to disable a plugin for a specific room.
|
||||||
|
- enable_plugin: Method to enable a plugin for a specific room.
|
||||||
|
- run: Method to initialize and run the bot.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- PLUGINS_DIR: Directory where plugins are stored
|
||||||
|
- PLUGINS: Dictionary to store loaded plugins
|
||||||
|
- config: Configuration object
|
||||||
|
- bot: Bot object
|
||||||
|
- disabled_plugins: Dictionary to store disabled plugins for each room
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.PLUGINS_DIR = "plugins"
|
"""
|
||||||
self.PLUGINS = {}
|
Constructor method for FunguyBot class.
|
||||||
self.config = None
|
"""
|
||||||
self.bot = None
|
# Setting up instance variables
|
||||||
self.load_dotenv()
|
self.PLUGINS_DIR = "plugins" # Directory where plugins are stored
|
||||||
self.setup_logging()
|
self.PLUGINS = {} # Dictionary to store loaded plugins
|
||||||
self.load_plugins()
|
self.config = None # Configuration object
|
||||||
self.load_config()
|
self.bot = None # Bot object
|
||||||
|
self.disabled_plugins = {} # Dictionary to store disabled plugins for each room
|
||||||
|
self.load_dotenv() # Loading environment variables from .env file
|
||||||
|
self.setup_logging() # Setting up logging configurations
|
||||||
|
self.load_plugins() # Loading plugins
|
||||||
|
self.load_config() # Loading bot configuration
|
||||||
|
self.load_disabled_plugins() # Loading disabled plugins from configuration file
|
||||||
|
|
||||||
def load_dotenv(self):
|
def load_dotenv(self):
|
||||||
|
"""
|
||||||
|
Method to load environment variables from a .env file.
|
||||||
|
"""
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
def setup_logging(self):
|
def setup_logging(self):
|
||||||
|
"""
|
||||||
|
Method to configure logging settings.
|
||||||
|
"""
|
||||||
|
# Basic configuration for logging messages to console
|
||||||
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO)
|
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO)
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
|
|
||||||
def load_plugins(self):
|
def load_plugins(self):
|
||||||
for plugin_file in os.listdir(self.PLUGINS_DIR):
|
"""
|
||||||
if plugin_file.endswith(".py"):
|
Method to load plugins from the specified directory.
|
||||||
plugin_name = os.path.splitext(plugin_file)[0]
|
"""
|
||||||
try:
|
# Iterating through whitelisted plugins only
|
||||||
module = importlib.import_module(f"{self.PLUGINS_DIR}.{plugin_name}")
|
for plugin_name in ALLOWED_PLUGINS:
|
||||||
self.PLUGINS[plugin_name] = module
|
plugin_file = os.path.join(self.PLUGINS_DIR, f"{plugin_name}.py")
|
||||||
logging.info(f"Loaded plugin: {plugin_name}")
|
|
||||||
except Exception as e:
|
# Verify that the plugin file exists
|
||||||
logging.error(f"Error loading plugin {plugin_name}: {e}")
|
if not os.path.isfile(plugin_file):
|
||||||
|
logging.warning(f"Plugin file not found: {plugin_file}, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Importing plugin module dynamically with validated plugin name
|
||||||
|
module = importlib.import_module(f"{self.PLUGINS_DIR}.{plugin_name}")
|
||||||
|
self.PLUGINS[plugin_name] = module # Storing loaded plugin module
|
||||||
|
logging.info(f"Loaded plugin: {plugin_name}") # Logging successful plugin loading
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error loading plugin {plugin_name}: {e}") # Logging error if plugin loading fails
|
||||||
|
|
||||||
def reload_plugins(self):
|
def reload_plugins(self):
|
||||||
self.PLUGINS = {}
|
"""
|
||||||
# Unload modules from sys.modules
|
Method to reload all plugins.
|
||||||
|
"""
|
||||||
|
self.PLUGINS = {} # Clearing loaded plugins dictionary
|
||||||
|
# Unloading modules from sys.modules
|
||||||
for plugin_name in list(sys.modules.keys()):
|
for plugin_name in list(sys.modules.keys()):
|
||||||
if plugin_name.startswith(self.PLUGINS_DIR + "."):
|
if plugin_name.startswith(self.PLUGINS_DIR + "."):
|
||||||
del sys.modules[plugin_name]
|
del sys.modules[plugin_name] # Deleting plugin module from system modules
|
||||||
self.load_plugins()
|
self.load_plugins() # Reloading plugins
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
self.config = FunguyConfig()
|
"""
|
||||||
|
Method to load configuration settings.
|
||||||
|
"""
|
||||||
|
self.config = FunguyConfig() # Creating instance of FunguyConfig to load configuration
|
||||||
|
|
||||||
|
def load_disabled_plugins(self):
|
||||||
|
"""
|
||||||
|
Method to load disabled plugins from configuration file.
|
||||||
|
"""
|
||||||
|
# Checking if configuration file exists
|
||||||
|
if os.path.exists('funguy.conf'):
|
||||||
|
# Loading configuration data from TOML file
|
||||||
|
with open('funguy.conf', 'r') as f:
|
||||||
|
config_data = toml.load(f)
|
||||||
|
# Extracting disabled plugins from configuration data
|
||||||
|
self.disabled_plugins = config_data.get('plugins', {}).get('disabled', {})
|
||||||
|
|
||||||
|
def save_disabled_plugins(self):
|
||||||
|
"""
|
||||||
|
Method to save disabled plugins to configuration file.
|
||||||
|
"""
|
||||||
|
existing_config = {}
|
||||||
|
# Checking if configuration file exists
|
||||||
|
if os.path.exists('funguy.conf'):
|
||||||
|
# Loading existing configuration data
|
||||||
|
with open('funguy.conf', 'r') as f:
|
||||||
|
existing_config = toml.load(f)
|
||||||
|
# Updating configuration data with disabled plugins
|
||||||
|
existing_config['plugins'] = {'disabled': self.disabled_plugins}
|
||||||
|
# Writing updated configuration data back to file
|
||||||
|
with open('funguy.conf', 'w') as f:
|
||||||
|
toml.dump(existing_config, f)
|
||||||
|
|
||||||
async def handle_commands(self, room, message):
|
async def handle_commands(self, room, message):
|
||||||
match = botlib.MessageMatch(room, message, self.bot, self.config.prefix)
|
"""
|
||||||
|
Method to handle incoming commands and dispatch them to appropriate plugins.
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, self.bot, self.config.prefix) # Matching message against bot's prefix
|
||||||
|
# Reloading plugins command
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("reload"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("reload"):
|
||||||
if str(message.sender) == self.config.admin_user:
|
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
||||||
self.reload_plugins()
|
self.reload_plugins() # Reloading plugins
|
||||||
await self.bot.api.send_text_message(room.room_id, "Plugins reloaded successfully")
|
await self.bot.api.send_text_message(room.room_id, "Plugins reloaded successfully") # Sending success message
|
||||||
else:
|
else:
|
||||||
await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.")
|
await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") # Sending unauthorized message
|
||||||
|
|
||||||
|
# Disable plugin command
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("disable"):
|
||||||
|
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
||||||
|
args = match.args() # Getting command arguments
|
||||||
|
if len(args) != 2: # Checking if correct number of arguments provided
|
||||||
|
await self.bot.api.send_text_message(room.room_id, "Usage: !disable <plugin> <room_id>") # Sending usage message
|
||||||
|
else:
|
||||||
|
plugin_name, room_id = args # Extracting plugin name and room ID
|
||||||
|
await self.disable_plugin(room_id, plugin_name) # Disabling plugin
|
||||||
|
await self.bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' disabled for room '{room_id}'") # Sending success message
|
||||||
|
else:
|
||||||
|
await self.bot.api.send_text_message(room.room_id, "You are not authorized to disable plugins.") # Sending unauthorized message
|
||||||
|
|
||||||
|
# Enable plugin command
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("enable"):
|
||||||
|
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
||||||
|
args = match.args() # Getting command arguments
|
||||||
|
if len(args) != 2: # Checking if correct number of arguments provided
|
||||||
|
await self.bot.api.send_text_message(room.room_id, "Usage: !enable <plugin> <room_id>") # Sending usage message
|
||||||
|
else:
|
||||||
|
plugin_name, room_id = args # Extracting plugin name and room ID
|
||||||
|
await self.enable_plugin(room_id, plugin_name) # Enabling plugin
|
||||||
|
await self.bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' enabled for room '{room_id}'") # Sending success message
|
||||||
|
else:
|
||||||
|
await self.bot.api.send_text_message(room.room_id, "You are not authorized to enable plugins.") # Sending unauthorized message
|
||||||
|
|
||||||
|
# Dispatching commands to plugins
|
||||||
for plugin_name, plugin_module in self.PLUGINS.items():
|
for plugin_name, plugin_module in self.PLUGINS.items():
|
||||||
await plugin_module.handle_command(room, message, self.bot, self.config.prefix, self.config)
|
if plugin_name not in self.disabled_plugins.get(room.room_id, []):
|
||||||
|
await plugin_module.handle_command(room, message, self.bot, self.config.prefix, self.config)
|
||||||
|
|
||||||
|
# Rehash config command
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"):
|
||||||
if str(message.sender) == self.config.admin_user:
|
if str(message.sender) == self.config.admin_user: # Checking if sender is admin user
|
||||||
self.rehash_config()
|
self.rehash_config() # Rehashing configuration
|
||||||
await self.bot.api.send_text_message(room.room_id, "Config rehashed")
|
await self.bot.api.send_text_message(room.room_id, "Config rehashed") # Sending success message
|
||||||
else:
|
else:
|
||||||
await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.")
|
await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") # Sending unauthorized message
|
||||||
|
|
||||||
def rehash_config(self):
|
def rehash_config(self):
|
||||||
del self.config
|
"""
|
||||||
self.config = FunguyConfig()
|
Method to rehash the configuration settings.
|
||||||
|
"""
|
||||||
|
del self.config # Deleting current configuration object
|
||||||
|
self.config = FunguyConfig() # Creating new instance of FunguyConfig to load updated configuration
|
||||||
|
|
||||||
|
async def disable_plugin(self, room_id, plugin_name):
|
||||||
|
"""
|
||||||
|
Method to disable a plugin for a specific room.
|
||||||
|
"""
|
||||||
|
if room_id not in self.disabled_plugins:
|
||||||
|
self.disabled_plugins[room_id] = [] # Creating entry for room ID if not exist
|
||||||
|
if plugin_name not in self.disabled_plugins[room_id]:
|
||||||
|
self.disabled_plugins[room_id].append(plugin_name) # Adding plugin to list of disabled plugins for the room
|
||||||
|
self.save_disabled_plugins() # Saving disabled plugins to configuration file
|
||||||
|
|
||||||
|
async def enable_plugin(self, room_id, plugin_name):
|
||||||
|
"""
|
||||||
|
Method to enable a plugin for a specific room.
|
||||||
|
"""
|
||||||
|
if room_id in self.disabled_plugins and plugin_name in self.disabled_plugins[room_id]:
|
||||||
|
self.disabled_plugins[room_id].remove(plugin_name) # Removing plugin from list of disabled plugins for the room
|
||||||
|
self.save_disabled_plugins() # Saving disabled plugins to configuration file
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
"""
|
||||||
|
Method to initialize and run the bot.
|
||||||
|
"""
|
||||||
|
# Retrieving Matrix credentials from environment variables
|
||||||
MATRIX_URL = os.getenv("MATRIX_URL")
|
MATRIX_URL = os.getenv("MATRIX_URL")
|
||||||
MATRIX_USER = os.getenv("MATRIX_USER")
|
MATRIX_USER = os.getenv("MATRIX_USER")
|
||||||
MATRIX_PASS = os.getenv("MATRIX_PASS")
|
MATRIX_PASS = os.getenv("MATRIX_PASS")
|
||||||
creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASS)
|
creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASS) # Creating credentials object
|
||||||
self.bot = botlib.Bot(creds, self.config)
|
self.bot = botlib.Bot(creds, self.config) # Creating bot instance
|
||||||
|
|
||||||
|
# Defining listener for message events
|
||||||
@self.bot.listener.on_message_event
|
@self.bot.listener.on_message_event
|
||||||
async def wrapper_handle_commands(room, message):
|
async def wrapper_handle_commands(room, message):
|
||||||
await self.handle_commands(room, message)
|
await self.handle_commands(room, message) # Calling handle_commands method for incoming messages
|
||||||
|
|
||||||
self.bot.run()
|
self.bot.run() # Running the bot
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bot = FunguyBot()
|
bot = FunguyBot() # Creating instance of FunguyBot
|
||||||
bot.run()
|
bot.run() # Running the bot
|
||||||
|
|
||||||
|
from plugins import cron # Import your cron plugin
|
||||||
|
|
||||||
|
# After bot starts running, periodically check for cron jobs
|
||||||
|
while True:
|
||||||
|
asyncio.sleep(60) # Check every minute (adjust as needed)
|
||||||
|
cron.run_cron_jobs(bot) # Check and execute cron jobs
|
||||||
|
@@ -1,78 +0,0 @@
|
|||||||
"""
|
|
||||||
This plugin provides a command to interact with the music knowledge A.I.
|
|
||||||
"""
|
|
||||||
# plugins/ai-music.py
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import simplematrixbotlib as botlib
|
|
||||||
import re
|
|
||||||
import markdown2
|
|
||||||
|
|
||||||
async def handle_command(room, message, bot, prefix, config):
|
|
||||||
"""
|
|
||||||
Function to handle the !music command.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
room (Room): The Matrix room where the command was invoked.
|
|
||||||
message (RoomMessage): The message object containing the command.
|
|
||||||
bot (Bot): The bot object.
|
|
||||||
PREFIX (str): The command prefix.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("music"):
|
|
||||||
logging.info("Received !music command")
|
|
||||||
args = match.args()
|
|
||||||
if len(args) < 1:
|
|
||||||
await bot.api.send_text_message(room.room_id, "Usage: !music [prompt]")
|
|
||||||
logging.info("Sent usage message to the room")
|
|
||||||
return
|
|
||||||
|
|
||||||
prompt = ' '.join(args)
|
|
||||||
|
|
||||||
# Prepare data for the API request
|
|
||||||
url = "http://127.0.0.1:5000/v1/completions"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"prompt": "<s>[INST]You are FunguyFlows, an AI music expert bot with access to a vast repository of knowledge encompassing every facet of music, spanning genres, artists, bands, compositions, albums, lyrics, music theory, composition techniques, composers, music history, music appreciation, and more. Your role is to serve as an invaluable resource and guide within the realm of music, offering comprehensive insights, recommendations, and assistance to music enthusiasts, students, professionals, and curious minds alike.Drawing upon humanity's collective knowledge and expertise in music, your database contains a wealth of information sourced from authoritative texts, scholarly articles, historical archives, musical compositions, biographies, discographies, and cultural repositories. This rich repository enables you to provide accurate, detailed, and insightful responses to a wide range of inquiries, covering an extensive array of topics related to music theory, composition, performance, history, and appreciation.As an AI music expert, your knowledge extends across various genres, including classical, jazz, rock, pop, hip-hop, electronic, folk, world music, and beyond. You possess a deep understanding of musical concepts such as melody, harmony, rhythm, timbre, form, dynamics, and texture, allowing you to analyze and interpret musical compositions with precision and clarity.In addition to your expertise in music theory and composition, you are well-versed in the works of renowned composers throughout history, from the classical masters of Bach, Mozart, and Beethoven to contemporary innovators like John Williams, Philip Glass, and Hans Zimmer. You can provide detailed biographical information, analysis of their compositions, and insights into their lasting impact on the world of music.Your knowledge of music history is extensive, spanning centuries of cultural evolution and musical innovation. From the Gregorian chants of the medieval period to the avant-garde experiments of the 20th century, you can trace the development of musical styles, movements, and traditions across different regions and epochs, shedding light on the social, political, and artistic contexts that shaped musical expression throughout history.Furthermore, your expertise encompasses a diverse range of topics related to music appreciation, including techniques for active listening, critical analysis of musical performances, understanding musical genres and styles, exploring the cultural significance of music, and engaging with music as a form of creative expression, emotional communication, and cultural identity.Whether users seek recommendations for discovering new artists and albums, assistance with analyzing musical compositions, insights into music theory concepts, guidance on composing their own music, or historical context for understanding musical traditions, you are poised to provide informative, engaging, and enriching responses tailored to their interests and inquiries.As an AI music expert bot, your mission is to inspire curiosity, deepen understanding, and foster appreciation for the diverse and multifaceted world of music. By sharing your knowledge, passion, and enthusiasm for music, you aim to empower individuals to explore, create, and connect through the universal language of sound. Embrace your role as a trusted guide and mentor within the realm of music, and let your expertise illuminate the path for music lovers and learners alike, one harmonious interaction at a time. You will only answer questions about music, and nothing else. Now... tell me about: "+prompt+"[/INST]",
|
|
||||||
"max_tokens": 1024,
|
|
||||||
"temperature": 1.31,
|
|
||||||
"top_p": 0.14,
|
|
||||||
"top_k": 49,
|
|
||||||
"seed": -1,
|
|
||||||
"stream": False,
|
|
||||||
"repetition_penalty": 1.17
|
|
||||||
}
|
|
||||||
|
|
||||||
# Make HTTP request to the API endpoint
|
|
||||||
try:
|
|
||||||
response = requests.post(url, headers=headers, json=data, verify=False)
|
|
||||||
response.raise_for_status() # Raise HTTPError for bad responses
|
|
||||||
payload = response.json()
|
|
||||||
new_text = payload['choices'][0]['text']
|
|
||||||
new_text = markdown_to_html(new_text)
|
|
||||||
print(new_text)
|
|
||||||
|
|
||||||
if new_text.count('<p>') > 1 or new_text.count('<li>') > 1: # Check if new_text has more than one paragraph
|
|
||||||
#new_text = new_text.replace("\n", '<br>')
|
|
||||||
#new_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", new_text)
|
|
||||||
new_text = "<details><summary><strong>🎵Funguy Music GPT🎵<br>⤵︎Click Here To See Funguy's Response⤵︎</strong></summary>" + new_text + "</details>"
|
|
||||||
await bot.api.send_markdown_message(room.room_id, new_text)
|
|
||||||
else:
|
|
||||||
await bot.api.send_markdown_message(room.room_id, new_text)
|
|
||||||
logging.info("Sent generated text to the room")
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logging.error(f"HTTP request failed for '{prompt}': {e}")
|
|
||||||
await bot.api.send_text_message(room.room_id, f"Error generating text: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def markdown_to_html(markdown_text):
|
|
||||||
html_content = markdown2.markdown(markdown_text)
|
|
||||||
return html_content
|
|
@@ -1,76 +0,0 @@
|
|||||||
"""
|
|
||||||
This plugin provides a command to interact with the LLM for Tech/IT/Security/Selfhosting/Programming etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# plugins/ai-tech.py
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import simplematrixbotlib as botlib
|
|
||||||
import re
|
|
||||||
import markdown2
|
|
||||||
|
|
||||||
async def handle_command(room, message, bot, prefix, config):
|
|
||||||
"""
|
|
||||||
Function to handle the !funguy command.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
room (Room): The Matrix room where the command was invoked.
|
|
||||||
message (RoomMessage): The message object containing the command.
|
|
||||||
bot (Bot): The bot object.
|
|
||||||
prefix (str): The command prefix.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("funguy"):
|
|
||||||
logging.info("Received !funguy command")
|
|
||||||
args = match.args()
|
|
||||||
if len(args) < 1:
|
|
||||||
await bot.api.send_text_message(room.room_id, "Usage: !funguy [prompt]")
|
|
||||||
logging.info("Sent usage message to the room")
|
|
||||||
return
|
|
||||||
|
|
||||||
prompt = ' '.join(args)
|
|
||||||
|
|
||||||
# Prepare data for the API request
|
|
||||||
url = "http://127.0.0.1:5000/v1/completions"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"prompt": "<s>[INST]You are FunguyGPT, a language model deployed as a chatbot for a community chat room hosted on Matrix. The chat room focuses on discussions related to self-hosting, system administration, cybersecurity, homelab setups, programming, coding, and general IT/tech topics. Your role is to assist users within the community by providing helpful responses and guidance on various technical matters. It's essential to keep your replies concise and relevant, addressing users' queries effectively while maintaining a friendly and approachable demeanor. Remember to prioritize clarity and brevity in your interactions to ensure a positive user experience within the chat room environment. You are FunguyGPT, an AI language model designed to serve as a chatbot within a vibrant and diverse community chat room hosted on the Matrix platform. This chat room acts as a hub for enthusiasts and professionals alike, engaging in discussions spanning a wide array of technical topics, including self-hosting, system administration, cybersecurity, homelab setups, programming, coding, and general IT/tech inquiries. Your primary objective is to act as a reliable and knowledgeable assistant, offering assistance, guidance, and solutions to the community members as they navigate through their technical challenges and endeavors.Given the broad spectrum of topics discussed within the community, it's crucial for you to possess a comprehensive understanding of various domains within the realm of technology. As such, your knowledge should encompass not only the fundamentals of programming languages, software development methodologies, and system administration principles but also extend to cybersecurity best practices, networking protocols, cloud computing, database management, and beyond. Your role as a chatbot is multifaceted and dynamic. You'll be tasked with responding to a wide range of queries, ranging from beginner-level inquiries seeking clarification on basic concepts to advanced discussions requiring nuanced insights and problem-solving skills. Whether it's troubleshooting code errors, configuring network settings, securing server environments, optimizing database performance, or recommending suitable homelab hardware, your goal is to provide accurate, actionable, and helpful responses tailored to the needs of the community members. In addition to offering direct assistance, you should also strive to foster a collaborative and supportive atmosphere within the chat room. Encourage knowledge sharing, facilitate discussions, and celebrate the achievements of community members as they tackle technical challenges and embark on learning journeys. By promoting a culture of learning and collaboration, you'll contribute to the growth and cohesion of the community, empowering individuals to expand their skill sets and achieve their goals within the realm of technology. As you engage with users within the chat room, prioritize brevity and clarity in your responses. While it's essential to provide comprehensive and accurate information, it's equally important to convey it in a concise and easily understandable manner. Avoid overly technical jargon or convoluted explanations that may confuse or overwhelm community members, opting instead for straightforward explanations and practical solutions whenever possible. Remember, your ultimate objective is to be a trusted ally and resource for the members of the community as they navigate the ever-evolving landscape of technology. By leveraging your expertise, empathy, and problem-solving abilities, you'll play a vital role in facilitating knowledge exchange, fostering collaboration, and empowering individuals to succeed in their technical endeavors. As you embark on this journey as a chatbot within the Matrix community, embrace the opportunity to make a meaningful and positive impact, one helpful interaction at a time. You will format the reply using minimal html instead of markdown. Do not use markdown formatting. Here is the prompt: "+prompt+"[/INST]",
|
|
||||||
"max_tokens": 1024,
|
|
||||||
"temperature": 1.31,
|
|
||||||
"top_p": 0.14,
|
|
||||||
"top_k": 49,
|
|
||||||
"seed": -1,
|
|
||||||
"stream": False,
|
|
||||||
"repetition_penalty": 1.17
|
|
||||||
}
|
|
||||||
|
|
||||||
# Make HTTP request to the API endpoint
|
|
||||||
try:
|
|
||||||
response = requests.post(url, headers=headers, json=data, verify=False)
|
|
||||||
response.raise_for_status() # Raise HTTPError for bad responses
|
|
||||||
payload = response.json()
|
|
||||||
new_text = payload['choices'][0]['text']
|
|
||||||
new_text = markdown_to_html(new_text)
|
|
||||||
if new_text.count('<p>') > 2: # Check if new_text has more than one paragraph
|
|
||||||
#new_text = new_text.replace("\n", '<br>')
|
|
||||||
#new_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", new_text)
|
|
||||||
new_text = "<details><summary><strong>🍄Funguy Tech GPT🍄<br>⤵︎Click Here To See Funguy's Response⤵︎</strong></summary>" + new_text + "</details>"
|
|
||||||
await bot.api.send_markdown_message(room.room_id, new_text)
|
|
||||||
else:
|
|
||||||
await bot.api.send_markdown_message(room.room_id, new_text)
|
|
||||||
logging.info(f"Sent generated text to the room: {new_text}")
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logging.error(f"HTTP request failed for '{prompt}': {e}")
|
|
||||||
await bot.api.send_text_message(room.room_id, f"Error generating text: {e}")
|
|
||||||
|
|
||||||
def markdown_to_html(markdown_text):
|
|
||||||
html_content = markdown2.markdown(markdown_text)
|
|
||||||
return html_content
|
|
1194
plugins/ai.json
Normal file
1194
plugins/ai.json
Normal file
File diff suppressed because it is too large
Load Diff
115
plugins/ai.py
Normal file
115
plugins/ai.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides commands to interact with different AI models.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
import re
|
||||||
|
import markdown2
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle AI commands.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix():
|
||||||
|
logging.info(f"Received command: {match.command()}")
|
||||||
|
|
||||||
|
command = match.command()
|
||||||
|
conf = load_config()
|
||||||
|
if command in conf:
|
||||||
|
await handle_ai_command(room, bot, command, match.args(), conf)
|
||||||
|
|
||||||
|
async def handle_ai_command(room, bot, command, args, config):
|
||||||
|
"""
|
||||||
|
Function to handle AI commands.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
command (str): The name of the AI model command.
|
||||||
|
args (list): List of arguments provided with the command.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
if len(args) < 1:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Usage: !{command} [prompt]")
|
||||||
|
logging.info("Sent usage message to the room")
|
||||||
|
return
|
||||||
|
|
||||||
|
prompt = ' '.join(args)
|
||||||
|
|
||||||
|
# Prepare data for the API request
|
||||||
|
url = "http://127.0.0.1:5000/v1/completions"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"prompt": f"<s>[INST]{config[command]['prompt']}{prompt}[/INST]",
|
||||||
|
"max_tokens": 4096,
|
||||||
|
"temperature": config[command]["temperature"],
|
||||||
|
"top_p": config[command]["top_p"],
|
||||||
|
"top_k": config[command]["top_k"],
|
||||||
|
"repetition_penalty": config[command]["repetition_penalty"],
|
||||||
|
"seed": -1,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make HTTP request to the API endpoint
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, json=data, verify=False, timeout=300)
|
||||||
|
response.raise_for_status() # Raise HTTPError for bad responses
|
||||||
|
payload = response.json()
|
||||||
|
new_text = payload['choices'][0]['text']
|
||||||
|
new_text = markdown_to_html(new_text)
|
||||||
|
|
||||||
|
if new_text.count('<p>') > 1 or new_text.count('<li>') > 1: # Check if new_text has more than one paragraph
|
||||||
|
new_text = f"<details><summary><strong>{config[command]['summary']}</strong></summary>{new_text}</details>"
|
||||||
|
await bot.api.send_markdown_message(room.room_id, new_text)
|
||||||
|
else:
|
||||||
|
await bot.api.send_markdown_message(room.room_id, new_text)
|
||||||
|
logging.info("Sent generated text to the room")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"HTTP request failed for '{prompt}': {e}")
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error generating text: {e}")
|
||||||
|
|
||||||
|
def markdown_to_html(markdown_text):
|
||||||
|
"""
|
||||||
|
Convert Markdown text to HTML.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
markdown_text (str): Markdown formatted text.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: HTML formatted text.
|
||||||
|
"""
|
||||||
|
html_content = markdown2.markdown(markdown_text)
|
||||||
|
return html_content
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
"""
|
||||||
|
Load configuration from ai.json file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Configuration parameters.
|
||||||
|
"""
|
||||||
|
with open("plugins/ai.json", "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
return config
|
||||||
|
|
||||||
|
CONFIG = load_config()
|
109
plugins/bitcoin.py
Normal file
109
plugins/bitcoin.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides a command to fetch the current Bitcoin price.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
|
||||||
|
BITCOIN_API_URL = "https://api.bitcointicker.co/trades/bitstamp/btcusd/60/"
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle the !btc command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("btc"):
|
||||||
|
logging.info("Received !btc command")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch Bitcoin price data
|
||||||
|
headers = {
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'User-Agent': 'FunguyBot/1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f"Fetching Bitcoin price from {BITCOIN_API_URL}")
|
||||||
|
response = requests.get(BITCOIN_API_URL, headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if not data or len(data) == 0:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"No Bitcoin price data available."
|
||||||
|
)
|
||||||
|
logging.warning("No Bitcoin price data returned from API")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the most recent trade (last item in the array)
|
||||||
|
latest_trade = data[-1]
|
||||||
|
price = latest_trade.get('price')
|
||||||
|
|
||||||
|
if price is None:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Could not extract Bitcoin price from API response."
|
||||||
|
)
|
||||||
|
logging.error("Price field not found in API response")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert to float and format with commas
|
||||||
|
try:
|
||||||
|
price_float = float(price)
|
||||||
|
price_formatted = f"${price_float:,.2f}"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
price_formatted = f"${price}"
|
||||||
|
|
||||||
|
# Optional: Get additional info if available
|
||||||
|
timestamp = latest_trade.get('timestamp', '')
|
||||||
|
volume = latest_trade.get('volume', '')
|
||||||
|
|
||||||
|
# Build the message
|
||||||
|
message_text = f"<strong>₿ BTC/USD</strong>"
|
||||||
|
message_text += f"<strong> Current Price:</strong> {price_formatted}"
|
||||||
|
|
||||||
|
message_text += ", <em>bitcointicker.co</em>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, message_text)
|
||||||
|
logging.info(f"Sent Bitcoin price: {price_formatted}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Request timed out. Bitcoin price API may be slow or unavailable."
|
||||||
|
)
|
||||||
|
logging.error("Bitcoin API timeout")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Error fetching Bitcoin price: {e}"
|
||||||
|
)
|
||||||
|
logging.error(f"Error fetching Bitcoin price: {e}")
|
||||||
|
|
||||||
|
except (KeyError, IndexError, ValueError) as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Error parsing Bitcoin price data."
|
||||||
|
)
|
||||||
|
logging.error(f"Error parsing Bitcoin API response: {e}", exc_info=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"An unexpected error occurred while fetching Bitcoin price."
|
||||||
|
)
|
||||||
|
logging.error(f"Unexpected error in Bitcoin plugin: {e}", exc_info=True)
|
@@ -98,13 +98,16 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
|
# Check if the message matches the command pattern and is not from this bot
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("set"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("set"):
|
||||||
|
# If the command is 'set', check if it has exactly two arguments
|
||||||
args = match.args()
|
args = match.args()
|
||||||
if len(args) != 2:
|
if len(args) != 2:
|
||||||
await bot.api.send_text_message(room.room_id, "Usage: !set <config_option> <value>")
|
await bot.api.send_text_message(room.room_id, "Usage: !set <config_option> <value>")
|
||||||
return
|
return
|
||||||
option, value = args
|
option, value = args
|
||||||
|
# Set the specified configuration option based on the provided value
|
||||||
if option == "admin_user":
|
if option == "admin_user":
|
||||||
config.admin_user = value
|
config.admin_user = value
|
||||||
await bot.api.send_text_message(room.room_id, f"Admin user set to {value}")
|
await bot.api.send_text_message(room.room_id, f"Admin user set to {value}")
|
||||||
@@ -114,6 +117,7 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
else:
|
else:
|
||||||
await bot.api.send_text_message(room.room_id, "Invalid configuration option")
|
await bot.api.send_text_message(room.room_id, "Invalid configuration option")
|
||||||
|
|
||||||
|
# If the command is 'get', retrieve the value of the specified configuration option
|
||||||
elif match.is_not_from_this_bot() and match.prefix() and match.command("get"):
|
elif match.is_not_from_this_bot() and match.prefix() and match.command("get"):
|
||||||
args = match.args()
|
args = match.args()
|
||||||
if len(args) != 1:
|
if len(args) != 1:
|
||||||
@@ -127,21 +131,24 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
else:
|
else:
|
||||||
await bot.api.send_text_message(room.room_id, "Invalid configuration option")
|
await bot.api.send_text_message(room.room_id, "Invalid configuration option")
|
||||||
|
|
||||||
|
# If the command is 'saveconf', save the current configuration
|
||||||
elif match.is_not_from_this_bot() and match.prefix() and match.command("saveconf"):
|
elif match.is_not_from_this_bot() and match.prefix() and match.command("saveconf"):
|
||||||
config.save_config(config.config_file)
|
config.save_config(config.config_file)
|
||||||
await bot.api.send_text_message(room.room_id, "Configuration saved")
|
await bot.api.send_text_message(room.room_id, "Configuration saved")
|
||||||
|
|
||||||
|
# If the command is 'loadconf', load the saved configuration
|
||||||
elif match.is_not_from_this_bot() and match.prefix() and match.command("loadconf"):
|
elif match.is_not_from_this_bot() and match.prefix() and match.command("loadconf"):
|
||||||
config.load_config(config.config_file)
|
config.load_config(config.config_file)
|
||||||
await bot.api.send_text_message(room.room_id, "Configuration loaded")
|
await bot.api.send_text_message(room.room_id, "Configuration loaded")
|
||||||
|
|
||||||
|
# If the command is 'show', display the current configuration
|
||||||
elif match.is_not_from_this_bot() and match.prefix() and match.command("show"):
|
elif match.is_not_from_this_bot() and match.prefix() and match.command("show"):
|
||||||
admin_user = config.admin_user
|
admin_user = config.admin_user
|
||||||
prefix = config.prefix
|
prefix = config.prefix
|
||||||
await bot.api.send_text_message(room.room_id, f"Admin user: {admin_user}, Prefix: {prefix}")
|
await bot.api.send_text_message(room.room_id, f"Admin user: {admin_user}, Prefix: {prefix}")
|
||||||
|
|
||||||
|
# If the command is 'reset', reset the configuration to default values
|
||||||
elif match.is_not_from_this_bot() and match.prefix() and match.command("reset"):
|
elif match.is_not_from_this_bot() and match.prefix() and match.command("reset"):
|
||||||
config.admin_user = ""
|
config.admin_user = ""
|
||||||
config.prefix = "!"
|
config.prefix = "!"
|
||||||
await bot.api.send_text_message(room.room_id, "Configuration reset")
|
await bot.api.send_text_message(room.room_id, "Configuration reset")
|
||||||
|
|
||||||
|
64
plugins/cron.py
Normal file
64
plugins/cron.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# plugins/cron.py
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from crontab import CronTab
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
|
||||||
|
# Database connection and cursor
|
||||||
|
conn = sqlite3.connect('cron.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Create table if not exists
|
||||||
|
cursor.execute('''CREATE TABLE IF NOT EXISTS cron (
|
||||||
|
room_id TEXT,
|
||||||
|
cron_entry TEXT,
|
||||||
|
command TEXT
|
||||||
|
)''')
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("cron"):
|
||||||
|
args = match.args()
|
||||||
|
if len(args) >= 4:
|
||||||
|
action = args[0]
|
||||||
|
room_id = args[1]
|
||||||
|
cron_entry = ' '.join(args[2:-1])
|
||||||
|
command = args[-1]
|
||||||
|
if action == "add":
|
||||||
|
add_cron(room_id, cron_entry, command)
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Cron added successfully")
|
||||||
|
elif action == "remove":
|
||||||
|
remove_cron(room_id, command)
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Cron removed successfully")
|
||||||
|
else:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Usage: !cron add|remove room_id cron_entry command")
|
||||||
|
|
||||||
|
def add_cron(room_id, cron_entry, command):
|
||||||
|
# Check if the cron entry already exists in the database for the given room_id and command
|
||||||
|
cursor.execute('SELECT * FROM cron WHERE room_id=? AND command=? AND cron_entry=?', (room_id, command, cron_entry))
|
||||||
|
existing_entry = cursor.fetchone()
|
||||||
|
if existing_entry:
|
||||||
|
return # Cron entry already exists, do not add duplicate
|
||||||
|
|
||||||
|
# Insert the cron entry into the database
|
||||||
|
cursor.execute('INSERT INTO cron (room_id, cron_entry, command) VALUES (?, ?, ?)', (room_id, cron_entry, command))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def remove_cron(room_id, command):
|
||||||
|
cursor.execute('DELETE FROM cron WHERE room_id=? AND command=?', (room_id, command))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
async def run_cron_jobs(bot):
|
||||||
|
cron = CronTab()
|
||||||
|
for job in cron:
|
||||||
|
cron_entry = str(job)
|
||||||
|
for row in cursor.execute('SELECT * FROM cron WHERE cron_entry=?', (cron_entry,)):
|
||||||
|
room_id, _, command = row
|
||||||
|
room = await bot.api.get_room_by_id(room_id)
|
||||||
|
if room:
|
||||||
|
plugin_name = command.split()[0].replace(prefix, '') # Extract plugin name
|
||||||
|
plugin_module = bot.plugins.get(plugin_name)
|
||||||
|
if plugin_module:
|
||||||
|
await plugin_module.handle_command(room, None, bot, prefix, config)
|
||||||
|
|
209
plugins/dns.py
Normal file
209
plugins/dns.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides a command to perform DNS reconnaissance on a domain.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import dns.resolver
|
||||||
|
import dns.reversename
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Common DNS record types to query
|
||||||
|
RECORD_TYPES = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA', 'PTR', 'SRV']
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_domain(domain):
|
||||||
|
"""
|
||||||
|
Validate if the provided string is a valid domain name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain (str): The domain to validate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if valid, False otherwise.
|
||||||
|
"""
|
||||||
|
# Basic domain validation regex
|
||||||
|
pattern = r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
|
||||||
|
return re.match(pattern, domain) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def format_dns_record(record_type, records):
|
||||||
|
"""
|
||||||
|
Format DNS records for display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
record_type (str): The type of DNS record.
|
||||||
|
records (list): List of DNS record values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted HTML string.
|
||||||
|
"""
|
||||||
|
if not records:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
output = f"<strong>{record_type} Records:</strong><br>"
|
||||||
|
for record in records:
|
||||||
|
output += f" • {record}<br>"
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
async def query_dns_records(domain):
|
||||||
|
"""
|
||||||
|
Query all common DNS record types for a domain.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain (str): The domain to query.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary with record types as keys and lists of records as values.
|
||||||
|
"""
|
||||||
|
results = {}
|
||||||
|
resolver = dns.resolver.Resolver()
|
||||||
|
resolver.timeout = 5
|
||||||
|
resolver.lifetime = 5
|
||||||
|
|
||||||
|
for record_type in RECORD_TYPES:
|
||||||
|
try:
|
||||||
|
logging.info(f"Querying {record_type} records for {domain}")
|
||||||
|
answers = resolver.resolve(domain, record_type)
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for rdata in answers:
|
||||||
|
if record_type == 'MX':
|
||||||
|
# MX records have preference and exchange
|
||||||
|
records.append(f"{rdata.preference} {rdata.exchange}")
|
||||||
|
elif record_type == 'SOA':
|
||||||
|
# SOA records have multiple fields
|
||||||
|
records.append(f"{rdata.mname} {rdata.rname}")
|
||||||
|
elif record_type == 'SRV':
|
||||||
|
# SRV records have priority, weight, port, and target
|
||||||
|
records.append(f"{rdata.priority} {rdata.weight} {rdata.port} {rdata.target}")
|
||||||
|
elif record_type == 'TXT':
|
||||||
|
# TXT records can have multiple strings
|
||||||
|
txt_data = ' '.join([s.decode() if isinstance(s, bytes) else str(s) for s in rdata.strings])
|
||||||
|
records.append(txt_data)
|
||||||
|
else:
|
||||||
|
records.append(str(rdata))
|
||||||
|
|
||||||
|
if records:
|
||||||
|
results[record_type] = records
|
||||||
|
logging.info(f"Found {len(records)} {record_type} record(s)")
|
||||||
|
|
||||||
|
except dns.resolver.NoAnswer:
|
||||||
|
logging.debug(f"No {record_type} records found for {domain}")
|
||||||
|
continue
|
||||||
|
except dns.resolver.NXDOMAIN:
|
||||||
|
logging.warning(f"Domain {domain} does not exist")
|
||||||
|
return None
|
||||||
|
except dns.resolver.Timeout:
|
||||||
|
logging.warning(f"Timeout querying {record_type} for {domain}")
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error querying {record_type} for {domain}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle the !dns command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("dns"):
|
||||||
|
logging.info("Received !dns command")
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
if len(args) != 1:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Usage: !dns <domain>\nExample: !dns example.com"
|
||||||
|
)
|
||||||
|
logging.info("Sent usage message for !dns")
|
||||||
|
return
|
||||||
|
|
||||||
|
domain = args[0].lower().strip()
|
||||||
|
|
||||||
|
# Remove protocol if present
|
||||||
|
domain = domain.replace('http://', '').replace('https://', '')
|
||||||
|
# Remove trailing slash if present
|
||||||
|
domain = domain.rstrip('/')
|
||||||
|
# Remove www. prefix if present (optional - you can keep it if you want)
|
||||||
|
# domain = domain.replace('www.', '')
|
||||||
|
|
||||||
|
# Validate domain
|
||||||
|
if not is_valid_domain(domain):
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Invalid domain name: {domain}"
|
||||||
|
)
|
||||||
|
logging.warning(f"Invalid domain provided: {domain}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info(f"Starting DNS reconnaissance for {domain}")
|
||||||
|
|
||||||
|
# Send "working on it" message for longer queries
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"🔍 Performing DNS reconnaissance on {domain}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Query DNS records
|
||||||
|
results = await query_dns_records(domain)
|
||||||
|
|
||||||
|
if results is None:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Domain {domain} does not exist (NXDOMAIN)"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"No DNS records found for {domain}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Format the output
|
||||||
|
output = f"<strong>🔍 DNS Records for {domain}</strong><br><br>"
|
||||||
|
|
||||||
|
# Order the records in a logical way
|
||||||
|
preferred_order = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SOA', 'SRV', 'PTR']
|
||||||
|
|
||||||
|
for record_type in preferred_order:
|
||||||
|
if record_type in results:
|
||||||
|
output += format_dns_record(record_type, results[record_type])
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Add any remaining record types not in preferred order
|
||||||
|
for record_type in results:
|
||||||
|
if record_type not in preferred_order:
|
||||||
|
output += format_dns_record(record_type, results[record_type])
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Wrap in collapsible details if output is large
|
||||||
|
if output.count('<br>') > 15:
|
||||||
|
output = f"<details><summary><strong>🔍 DNS Records for {domain}</strong></summary>{output}</details>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
logging.info(f"Sent DNS records for {domain}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"An error occurred while performing DNS lookup: {str(e)}"
|
||||||
|
)
|
||||||
|
logging.error(f"Error in DNS plugin for {domain}: {e}", exc_info=True)
|
270
plugins/dnsdumpster.py
Normal file
270
plugins/dnsdumpster.py
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides DNSDumpster.com integration for domain reconnaissance and DNS mapping.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
parent_dir = os.path.dirname(plugin_dir)
|
||||||
|
dotenv_path = os.path.join(parent_dir, '.env')
|
||||||
|
load_dotenv(dotenv_path)
|
||||||
|
|
||||||
|
DNSDUMPSTER_API_KEY = os.getenv("DNSDUMPSTER_KEY", "")
|
||||||
|
DNSDUMPSTER_API_BASE = "https://api.dnsdumpster.com"
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle DNSDumpster commands.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("dnsdumpster"):
|
||||||
|
logging.info("Received !dnsdumpster command")
|
||||||
|
|
||||||
|
# Check if API key is configured
|
||||||
|
if not DNSDUMPSTER_API_KEY:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"DNSDumpster API key not configured. Please set DNSDUMPSTER_KEY environment variable."
|
||||||
|
)
|
||||||
|
logging.error("DNSDumpster API key not configured")
|
||||||
|
return
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
if len(args) < 1:
|
||||||
|
await show_usage(room, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if it's a test command or domain lookup
|
||||||
|
if args[0].lower() == "test":
|
||||||
|
await test_dnsdumpster_connection(room, bot)
|
||||||
|
else:
|
||||||
|
# Treat the first argument as the domain
|
||||||
|
domain = args[0].lower().strip()
|
||||||
|
await dnsdumpster_domain_lookup(room, bot, domain)
|
||||||
|
|
||||||
|
async def show_usage(room, bot):
|
||||||
|
"""Display DNSDumpster command usage."""
|
||||||
|
usage = """
|
||||||
|
<strong>🔍 DNSDumpster Commands:</strong>
|
||||||
|
|
||||||
|
<strong>!dnsdumpster <domain_name></strong> - Get comprehensive DNS reconnaissance for a domain
|
||||||
|
<strong>!dnsdumpster test</strong> - Test API connection
|
||||||
|
|
||||||
|
<strong>Examples:</strong>
|
||||||
|
• <code>!dnsdumpster google.com</code>
|
||||||
|
• <code>!dnsdumpster github.com</code>
|
||||||
|
• <code>!dnsdumpster example.com</code>
|
||||||
|
|
||||||
|
<strong>Rate Limit:</strong> 1 request per 2 seconds
|
||||||
|
"""
|
||||||
|
await bot.api.send_markdown_message(room.room_id, usage)
|
||||||
|
|
||||||
|
async def test_dnsdumpster_connection(room, bot):
|
||||||
|
"""Test DNSDumpster API connection."""
|
||||||
|
try:
|
||||||
|
test_domain = "google.com" # Changed from example.com to google.com
|
||||||
|
url = f"{DNSDUMPSTER_API_BASE}/domain/{test_domain}"
|
||||||
|
headers = {
|
||||||
|
"X-API-Key": DNSDUMPSTER_API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f"Testing DNSDumpster API with domain: {test_domain}")
|
||||||
|
response = requests.get(url, headers=headers, timeout=15)
|
||||||
|
|
||||||
|
debug_info = f"<strong>🔧 DNSDumpster API Test</strong><br>"
|
||||||
|
debug_info += f"<strong>Status Code:</strong> {response.status_code}<br>"
|
||||||
|
debug_info += f"<strong>Test Domain:</strong> {test_domain}<br>"
|
||||||
|
debug_info += f"<strong>Headers Used:</strong> X-API-Key<br>"
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
debug_info += "<strong>✅ SUCCESS - API is working!</strong><br>"
|
||||||
|
debug_info += f"<strong>Response Keys:</strong> {list(data.keys())}<br>"
|
||||||
|
|
||||||
|
# Show some sample data
|
||||||
|
if data.get('a'):
|
||||||
|
debug_info += f"<strong>A Records Found:</strong> {len(data['a'])}<br>"
|
||||||
|
if data.get('ns'):
|
||||||
|
debug_info += f"<strong>NS Records Found:</strong> {len(data['ns'])}<br>"
|
||||||
|
if data.get('total_a_recs'):
|
||||||
|
debug_info += f"<strong>Total A Records:</strong> {data['total_a_recs']}<br>"
|
||||||
|
|
||||||
|
elif response.status_code == 400:
|
||||||
|
debug_info += "<strong>❌ Bad Request - Check domain format</strong><br>"
|
||||||
|
debug_info += f"<strong>Response:</strong> {response.text[:200]}<br>"
|
||||||
|
elif response.status_code == 401:
|
||||||
|
debug_info += "<strong>❌ Unauthorized - Invalid API key</strong><br>"
|
||||||
|
elif response.status_code == 429:
|
||||||
|
debug_info += "<strong>⚠️ Rate Limit Exceeded - Wait 2 seconds</strong><br>"
|
||||||
|
else:
|
||||||
|
debug_info += f"<strong>❌ Error:</strong> {response.status_code} - {response.text[:200]}<br>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, debug_info)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Test failed: {str(e)}")
|
||||||
|
|
||||||
|
async def dnsdumpster_domain_lookup(room, bot, domain):
|
||||||
|
"""Get comprehensive DNS reconnaissance for a domain."""
|
||||||
|
try:
|
||||||
|
url = f"{DNSDUMPSTER_API_BASE}/domain/{domain}"
|
||||||
|
headers = {
|
||||||
|
"X-API-Key": DNSDUMPSTER_API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f"Fetching DNSDumpster data for domain: {domain}")
|
||||||
|
|
||||||
|
# Send initial processing message
|
||||||
|
await bot.api.send_text_message(room.room_id, f"🔍 Processing DNS reconnaissance for {domain}...")
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers, timeout=30)
|
||||||
|
|
||||||
|
if response.status_code == 400:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Bad request - check domain format: {domain}")
|
||||||
|
return
|
||||||
|
elif response.status_code == 401:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Invalid DNSDumpster API key")
|
||||||
|
return
|
||||||
|
elif response.status_code == 403:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Access denied - check API key permissions")
|
||||||
|
return
|
||||||
|
elif response.status_code == 429:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Rate limit exceeded - wait 2 seconds between requests")
|
||||||
|
return
|
||||||
|
elif response.status_code != 200:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"DNSDumpster API error: {response.status_code} - {response.text[:100]}")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
logging.info(f"DNSDumpster response keys: {list(data.keys())}")
|
||||||
|
|
||||||
|
# Format the comprehensive DNS report
|
||||||
|
output = await format_dnsdumpster_report(domain, data)
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
logging.info(f"Sent DNSDumpster data for {domain}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await bot.api.send_text_message(room.room_id, "DNSDumpster API request timed out")
|
||||||
|
logging.error("DNSDumpster API timeout")
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error fetching DNSDumpster data: {str(e)}")
|
||||||
|
logging.error(f"Error in dnsdumpster_domain_lookup: {e}")
|
||||||
|
|
||||||
|
async def format_dnsdumpster_report(domain, data):
|
||||||
|
"""Format DNSDumpster JSON response into a readable report."""
|
||||||
|
output = f"<strong>🔍 DNSDumpster Report: {domain}</strong><br><br>"
|
||||||
|
|
||||||
|
# Summary statistics
|
||||||
|
if data.get('total_a_recs'):
|
||||||
|
output += f"<strong>📊 Summary</strong><br>"
|
||||||
|
output += f" • <strong>Total A Records:</strong> {data['total_a_recs']}<br>"
|
||||||
|
|
||||||
|
# A Records - Show ALL records
|
||||||
|
if data.get('a') and data['a']:
|
||||||
|
output += f"<br><strong>📍 A Records (IPv4) - {len(data['a'])} found</strong><br>"
|
||||||
|
for record in data['a']: # Show ALL A records
|
||||||
|
host = record.get('host', 'N/A')
|
||||||
|
ips = record.get('ips', [])
|
||||||
|
|
||||||
|
output += f" • <strong>{host}</strong><br>"
|
||||||
|
for ip_info in ips: # Show ALL IPs per host
|
||||||
|
ip = ip_info.get('ip', 'N/A')
|
||||||
|
country = ip_info.get('country', 'Unknown')
|
||||||
|
asn_name = ip_info.get('asn_name', 'Unknown')
|
||||||
|
|
||||||
|
output += f" └─ {ip} ({country})<br>"
|
||||||
|
output += f" └─ {asn_name}<br>"
|
||||||
|
|
||||||
|
# Show banner information if available
|
||||||
|
banners = ip_info.get('banners', {})
|
||||||
|
if banners.get('http') or banners.get('https'):
|
||||||
|
output += f" └─ <em>Web Services:</em> "
|
||||||
|
services = []
|
||||||
|
if banners.get('http'):
|
||||||
|
services.append("HTTP")
|
||||||
|
if banners.get('https'):
|
||||||
|
services.append("HTTPS")
|
||||||
|
output += f"{', '.join(services)}<br>"
|
||||||
|
|
||||||
|
# NS Records - Show ALL records
|
||||||
|
if data.get('ns') and data['ns']:
|
||||||
|
output += f"<br><strong>🔗 NS Records (Name Servers) - {len(data['ns'])} found</strong><br>"
|
||||||
|
for record in data['ns']: # Show ALL NS records
|
||||||
|
host = record.get('host', 'N/A')
|
||||||
|
ips = record.get('ips', [])
|
||||||
|
|
||||||
|
output += f" • <strong>{host}</strong><br>"
|
||||||
|
for ip_info in ips: # Show ALL IPs
|
||||||
|
ip = ip_info.get('ip', 'N/A')
|
||||||
|
country = ip_info.get('country', 'Unknown')
|
||||||
|
output += f" └─ {ip} ({country})<br>"
|
||||||
|
|
||||||
|
# MX Records - Show ALL records
|
||||||
|
if data.get('mx') and data['mx']:
|
||||||
|
output += f"<br><strong>📧 MX Records (Mail Servers) - {len(data['mx'])} found</strong><br>"
|
||||||
|
for record in data['mx']: # Show ALL MX records
|
||||||
|
host = record.get('host', 'N/A')
|
||||||
|
ips = record.get('ips', [])
|
||||||
|
|
||||||
|
output += f" • <strong>{host}</strong><br>"
|
||||||
|
for ip_info in ips: # Show ALL IPs
|
||||||
|
ip = ip_info.get('ip', 'N/A')
|
||||||
|
country = ip_info.get('country', 'Unknown')
|
||||||
|
output += f" └─ {ip} ({country})<br>"
|
||||||
|
|
||||||
|
# CNAME Records - Show ALL records
|
||||||
|
if data.get('cname') and data['cname']:
|
||||||
|
output += f"<br><strong>🔀 CNAME Records - {len(data['cname'])} found</strong><br>"
|
||||||
|
for record in data['cname']: # Show ALL CNAME records
|
||||||
|
host = record.get('host', 'N/A')
|
||||||
|
target = record.get('target', 'N/A')
|
||||||
|
output += f" • {host} → {target}<br>"
|
||||||
|
|
||||||
|
# TXT Records - Show ALL records
|
||||||
|
if data.get('txt') and data['txt']:
|
||||||
|
output += f"<br><strong>📄 TXT Records - {len(data['txt'])} found</strong><br>"
|
||||||
|
for txt in data['txt']: # Show ALL TXT records
|
||||||
|
# Truncate very long TXT records but show more content
|
||||||
|
if len(txt) > 200:
|
||||||
|
txt = txt[:200] + "..."
|
||||||
|
output += f" • {txt}<br>"
|
||||||
|
|
||||||
|
# Additional record types that might be present - Show ALL records
|
||||||
|
other_records = ['aaaa', 'srv', 'soa', 'ptr']
|
||||||
|
for record_type in other_records:
|
||||||
|
if data.get(record_type) and data[record_type]:
|
||||||
|
output += f"<br><strong>🔧 {record_type.upper()} Records - {len(data[record_type])} found</strong><br>"
|
||||||
|
for record in data[record_type]: # Show ALL records
|
||||||
|
if isinstance(record, dict):
|
||||||
|
# Format dictionary records nicely
|
||||||
|
record_str = ", ".join([f"{k}: {v}" for k, v in record.items()])
|
||||||
|
if len(record_str) > 150:
|
||||||
|
record_str = record_str[:150] + "..."
|
||||||
|
output += f" • {record_str}<br>"
|
||||||
|
else:
|
||||||
|
output += f" • {record}<br>"
|
||||||
|
|
||||||
|
# Add rate limit reminder
|
||||||
|
output += "<br><em>💡 Rate Limit: 1 request per 2 seconds</em>"
|
||||||
|
|
||||||
|
# Always wrap in collapsible details since we're showing all results
|
||||||
|
output = f"<details><summary><strong>🔍 DNSDumpster Report: {domain} (Click to expand)</strong></summary>{output}</details>"
|
||||||
|
|
||||||
|
return output
|
238
plugins/exploitdb.py
Normal file
238
plugins/exploitdb.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides a command to search Exploit-DB for security exploits and vulnerabilities.
|
||||||
|
Uses the searchsploit-style approach with the files.csv database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Exploit-DB CSV database URL
|
||||||
|
EXPLOITDB_CSV_URL = "https://gitlab.com/exploit-database/exploitdb/-/raw/main/files_exploits.csv"
|
||||||
|
|
||||||
|
|
||||||
|
def format_exploit(exploit, index, total):
|
||||||
|
"""
|
||||||
|
Format an exploit entry for display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exploit (dict): The exploit data.
|
||||||
|
index (int): Current result index.
|
||||||
|
total (int): Total number of results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted HTML string.
|
||||||
|
"""
|
||||||
|
edb_id = exploit.get('id', 'N/A')
|
||||||
|
title = exploit.get('description', 'No title')
|
||||||
|
date = exploit.get('date', 'Unknown')
|
||||||
|
author = exploit.get('author', 'Unknown')
|
||||||
|
exploit_type = exploit.get('type', 'Unknown')
|
||||||
|
platform = exploit.get('platform', 'Unknown')
|
||||||
|
|
||||||
|
# Build the URL
|
||||||
|
url = f"https://www.exploit-db.com/exploits/{edb_id}"
|
||||||
|
|
||||||
|
output = f"""<strong>💣 Exploit {index}/{total}</strong><br>
|
||||||
|
<strong>Title:</strong> {title}<br>
|
||||||
|
<strong>EDB-ID:</strong> {edb_id}<br>
|
||||||
|
<strong>Type:</strong> {exploit_type} | <strong>Platform:</strong> {platform}<br>
|
||||||
|
<strong>Author:</strong> {author} | <strong>Date:</strong> {date}<br>
|
||||||
|
<strong>URL:</strong> <a href="{url}">{url}</a>"""
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
async def search_exploitdb_csv(query, max_results=5):
|
||||||
|
"""
|
||||||
|
Search Exploit-DB CSV database for exploits matching the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): Search term.
|
||||||
|
max_results (int): Maximum number of results to return.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of exploit dictionaries, or None on error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logging.info(f"Downloading Exploit-DB CSV database...")
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'FunguyBot/1.0',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download the CSV file
|
||||||
|
response = requests.get(EXPLOITDB_CSV_URL, headers=headers, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Parse CSV
|
||||||
|
csv_data = response.text
|
||||||
|
csv_file = io.StringIO(csv_data)
|
||||||
|
reader = csv.DictReader(csv_file)
|
||||||
|
|
||||||
|
# Search through CSV
|
||||||
|
results = []
|
||||||
|
query_lower = query.lower()
|
||||||
|
|
||||||
|
logging.info(f"Searching CSV for: {query}")
|
||||||
|
|
||||||
|
for row in reader:
|
||||||
|
# Search in description (title) and other fields
|
||||||
|
description = row.get('description', '').lower()
|
||||||
|
file_path = row.get('file', '').lower()
|
||||||
|
|
||||||
|
if query_lower in description or query_lower in file_path:
|
||||||
|
exploit = {
|
||||||
|
'id': row.get('id', 'N/A'),
|
||||||
|
'description': row.get('description', 'No title'),
|
||||||
|
'date': row.get('date_published', row.get('date', 'Unknown')),
|
||||||
|
'author': row.get('author', 'Unknown'),
|
||||||
|
'type': row.get('type', 'Unknown'),
|
||||||
|
'platform': row.get('platform', 'Unknown')
|
||||||
|
}
|
||||||
|
results.append(exploit)
|
||||||
|
|
||||||
|
if len(results) >= max_results:
|
||||||
|
break
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logging.error("Timeout downloading Exploit-DB database")
|
||||||
|
return None
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Error downloading Exploit-DB database: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Unexpected error searching Exploit-DB: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def search_exploitdb_google(query, max_results=5):
|
||||||
|
"""
|
||||||
|
Alternative: Search Exploit-DB using site-specific search.
|
||||||
|
Returns formatted search URLs instead of parsing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): Search term.
|
||||||
|
max_results (int): Maximum number of results to return.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted search information.
|
||||||
|
"""
|
||||||
|
# Create search URLs
|
||||||
|
exploitdb_search_url = f"https://www.exploit-db.com/search?q={query}"
|
||||||
|
google_search_url = f"https://www.google.com/search?q=site:exploit-db.com+{query}"
|
||||||
|
|
||||||
|
output = f"""<strong>💣 Exploit-DB Search for: {query}</strong><br><br>
|
||||||
|
<strong>Direct Search:</strong><br>
|
||||||
|
<a href="{exploitdb_search_url}">{exploitdb_search_url}</a><br><br>
|
||||||
|
<strong>Google Site Search:</strong><br>
|
||||||
|
<a href="{google_search_url}">{google_search_url}</a><br><br>
|
||||||
|
<em>💡 Tip: You can also use <code>searchsploit</code> command-line tool for offline searches.</em><br>
|
||||||
|
<em>⚠️ Use responsibly and only on systems you have permission to test.</em>"""
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle the !exploitdb command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("exploitdb"):
|
||||||
|
logging.info("Received !exploitdb command")
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
if len(args) < 1:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Usage: !exploitdb <search term> [max_results]\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" !exploitdb wordpress\n"
|
||||||
|
" !exploitdb apache 3\n"
|
||||||
|
" !exploitdb windows privilege escalation\n"
|
||||||
|
"Searches Exploit-DB for security vulnerabilities and exploits."
|
||||||
|
)
|
||||||
|
logging.info("Sent usage message for !exploitdb")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if last argument is a number (max results)
|
||||||
|
max_results = 5
|
||||||
|
search_terms = args
|
||||||
|
|
||||||
|
if args[-1].isdigit():
|
||||||
|
max_results = int(args[-1])
|
||||||
|
if max_results < 1:
|
||||||
|
max_results = 1
|
||||||
|
elif max_results > 10:
|
||||||
|
max_results = 10
|
||||||
|
search_terms = args[:-1]
|
||||||
|
|
||||||
|
query = ' '.join(search_terms)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send "searching" message
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"🔍 Searching Exploit-DB for: {query}... (this may take a moment)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try CSV search first
|
||||||
|
exploits = await search_exploitdb_csv(query, max_results)
|
||||||
|
|
||||||
|
if exploits is None:
|
||||||
|
# Fallback to providing search links
|
||||||
|
logging.warning("CSV search failed, providing search links instead")
|
||||||
|
output = await search_exploitdb_google(query, max_results)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not exploits:
|
||||||
|
# Also provide search links when no results
|
||||||
|
output = f"No exploits found in local search for: <strong>{query}</strong><br><br>"
|
||||||
|
output += await search_exploitdb_google(query, max_results)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
logging.info(f"No exploits found for: {query}")
|
||||||
|
return
|
||||||
|
|
||||||
|
total = len(exploits)
|
||||||
|
logging.info(f"Found {total} exploit(s) for: {query}")
|
||||||
|
|
||||||
|
# Format all results
|
||||||
|
output = f"<strong>💣 Exploit-DB Search Results for: {query}</strong><br><br>"
|
||||||
|
|
||||||
|
for idx, exploit in enumerate(exploits, 1):
|
||||||
|
output += format_exploit(exploit, idx, total)
|
||||||
|
output += "<br><br>"
|
||||||
|
|
||||||
|
output += f"<em>⚠️ Use responsibly and only on systems you have permission to test.</em>"
|
||||||
|
|
||||||
|
# Wrap in collapsible details if more than 2 results
|
||||||
|
if total > 2:
|
||||||
|
summary = f"<strong>💣 Exploit-DB: {query}</strong> ({total} results)"
|
||||||
|
output = f"<details><summary>{summary}</summary>{output}</details>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
logging.info(f"Sent {total} exploit(s) for: {query}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"An error occurred while searching Exploit-DB: {str(e)}"
|
||||||
|
)
|
||||||
|
logging.error(f"Error in exploitdb plugin: {e}", exc_info=True)
|
386
plugins/hashid.py
Normal file
386
plugins/hashid.py
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides a command to identify hash types using comprehensive pattern matching.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
|
||||||
|
def identify_hash(hash_string):
|
||||||
|
"""
|
||||||
|
Identify the hash type based on comprehensive pattern matching.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hash_string (str): The hash string to identify
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of tuples (hash_type, hashcat_mode, john_format, confidence)
|
||||||
|
"""
|
||||||
|
|
||||||
|
hash_string = hash_string.strip()
|
||||||
|
hash_lower = hash_string.lower()
|
||||||
|
length = len(hash_string)
|
||||||
|
|
||||||
|
possible_types = []
|
||||||
|
|
||||||
|
# Unix crypt and modular crypt formats (most specific first)
|
||||||
|
if hash_string.startswith('$'):
|
||||||
|
# yescrypt (modern Linux /etc/shadow)
|
||||||
|
if re.match(r'^\$y\$', hash_string):
|
||||||
|
possible_types.append(("yescrypt", None, "yescrypt", 95))
|
||||||
|
|
||||||
|
# scrypt
|
||||||
|
elif re.match(r'^\$7\$', hash_string):
|
||||||
|
possible_types.append(("scrypt", "8900", "scrypt", 95))
|
||||||
|
|
||||||
|
# Argon2
|
||||||
|
elif re.match(r'^\$argon2(id?|d)\$', hash_string):
|
||||||
|
if '$argon2i$' in hash_string:
|
||||||
|
possible_types.append(("Argon2i", "10900", "argon2", 95))
|
||||||
|
elif '$argon2d$' in hash_string:
|
||||||
|
possible_types.append(("Argon2d", None, "argon2", 95))
|
||||||
|
elif '$argon2id$' in hash_string:
|
||||||
|
possible_types.append(("Argon2id", "10900", "argon2", 95))
|
||||||
|
|
||||||
|
# bcrypt variants
|
||||||
|
elif re.match(r'^\$(2[abxy]?)\$', hash_string):
|
||||||
|
bcrypt_type = re.match(r'^\$(2[abxy]?)\$', hash_string).group(1)
|
||||||
|
possible_types.append((f"bcrypt ({bcrypt_type})", "3200", "bcrypt", 95))
|
||||||
|
|
||||||
|
# SHA-512 Crypt (common in Linux)
|
||||||
|
elif re.match(r'^\$6\$', hash_string):
|
||||||
|
possible_types.append(("SHA-512 Crypt (Unix)", "1800", "sha512crypt", 95))
|
||||||
|
|
||||||
|
# SHA-256 Crypt (Unix)
|
||||||
|
elif re.match(r'^\$5\$', hash_string):
|
||||||
|
possible_types.append(("SHA-256 Crypt (Unix)", "7400", "sha256crypt", 95))
|
||||||
|
|
||||||
|
# MD5 Crypt (Unix)
|
||||||
|
elif re.match(r'^\$1\$', hash_string):
|
||||||
|
possible_types.append(("MD5 Crypt (Unix)", "500", "md5crypt", 95))
|
||||||
|
|
||||||
|
# Apache MD5
|
||||||
|
elif re.match(r'^\$apr1\$', hash_string):
|
||||||
|
possible_types.append(("Apache MD5 (apr1)", "1600", "md5crypt", 95))
|
||||||
|
|
||||||
|
# AIX SMD5
|
||||||
|
elif re.match(r'^\{smd5\}', hash_string, re.IGNORECASE):
|
||||||
|
possible_types.append(("AIX {smd5}", "6300", None, 90))
|
||||||
|
|
||||||
|
# AIX SSHA256
|
||||||
|
elif re.match(r'^\{ssha256\}', hash_string, re.IGNORECASE):
|
||||||
|
possible_types.append(("AIX {ssha256}", "6700", None, 90))
|
||||||
|
|
||||||
|
# AIX SSHA512
|
||||||
|
elif re.match(r'^\{ssha512\}', hash_string, re.IGNORECASE):
|
||||||
|
possible_types.append(("AIX {ssha512}", "6800", None, 90))
|
||||||
|
|
||||||
|
# phpBB3
|
||||||
|
elif re.match(r'^\$H\$', hash_string):
|
||||||
|
possible_types.append(("phpBB3", "400", "phpass", 90))
|
||||||
|
|
||||||
|
# Wordpress
|
||||||
|
elif re.match(r'^\$P\$', hash_string):
|
||||||
|
possible_types.append(("Wordpress", "400", "phpass", 90))
|
||||||
|
|
||||||
|
# Drupal 7+
|
||||||
|
elif re.match(r'^\$S\$', hash_string):
|
||||||
|
possible_types.append(("Drupal 7+", "7900", "drupal7", 90))
|
||||||
|
|
||||||
|
# WBB3 (Woltlab Burning Board)
|
||||||
|
elif re.match(r'^\$wbb3\$', hash_string):
|
||||||
|
possible_types.append(("WBB3 (Woltlab)", None, None, 85))
|
||||||
|
|
||||||
|
# PBKDF2-HMAC-SHA256
|
||||||
|
elif re.match(r'^\$pbkdf2-sha256\$', hash_string):
|
||||||
|
possible_types.append(("PBKDF2-HMAC-SHA256", "10900", "pbkdf2-hmac-sha256", 90))
|
||||||
|
|
||||||
|
# PBKDF2-HMAC-SHA512
|
||||||
|
elif re.match(r'^\$pbkdf2-sha512\$', hash_string):
|
||||||
|
possible_types.append(("PBKDF2-HMAC-SHA512", None, "pbkdf2-hmac-sha512", 90))
|
||||||
|
|
||||||
|
# Django PBKDF2
|
||||||
|
elif re.match(r'^pbkdf2_sha256\$', hash_string):
|
||||||
|
possible_types.append(("Django PBKDF2-SHA256", "10000", "django", 90))
|
||||||
|
|
||||||
|
# Unknown modular crypt format
|
||||||
|
else:
|
||||||
|
possible_types.append(("Unknown Modular Crypt Format", None, None, 30))
|
||||||
|
|
||||||
|
return possible_types
|
||||||
|
|
||||||
|
# LDAP formats
|
||||||
|
if hash_string.startswith('{'):
|
||||||
|
if re.match(r'^\{SHA\}', hash_string, re.IGNORECASE):
|
||||||
|
possible_types.append(("LDAP SHA-1", "101", "nsldap", 90))
|
||||||
|
elif re.match(r'^\{SSHA\}', hash_string, re.IGNORECASE):
|
||||||
|
possible_types.append(("LDAP SSHA (Salted SHA-1)", "111", "nsldaps", 90))
|
||||||
|
elif re.match(r'^\{MD5\}', hash_string, re.IGNORECASE):
|
||||||
|
possible_types.append(("LDAP MD5", "3210", None, 90))
|
||||||
|
elif re.match(r'^\{SMD5\}', hash_string, re.IGNORECASE):
|
||||||
|
possible_types.append(("LDAP SMD5 (Salted MD5)", "3211", None, 90))
|
||||||
|
elif re.match(r'^\{CRYPT\}', hash_string, re.IGNORECASE):
|
||||||
|
possible_types.append(("LDAP CRYPT", None, None, 85))
|
||||||
|
return possible_types
|
||||||
|
|
||||||
|
# Check for colon-separated formats (LM:NTLM, username:hash, etc.)
|
||||||
|
if ':' in hash_string:
|
||||||
|
parts = hash_string.split(':')
|
||||||
|
|
||||||
|
# NetNTLMv1 / NetNTLMv2
|
||||||
|
if len(parts) >= 5:
|
||||||
|
possible_types.append(("NetNTLMv2", "5600", "netntlmv2", 85))
|
||||||
|
possible_types.append(("NetNTLMv1", "5500", "netntlm", 75))
|
||||||
|
|
||||||
|
# LM:NTLM format
|
||||||
|
elif len(parts) == 2 and len(parts[0]) == 32 and len(parts[1]) == 32:
|
||||||
|
possible_types.append(("LM:NTLM", "1000", "nt", 90))
|
||||||
|
|
||||||
|
# Username:Hash or similar
|
||||||
|
elif len(parts) == 2:
|
||||||
|
hash_part = parts[1]
|
||||||
|
if len(hash_part) == 32:
|
||||||
|
possible_types.append(("NTLM (with username)", "1000", "nt", 80))
|
||||||
|
elif len(hash_part) == 40:
|
||||||
|
possible_types.append(("SHA-1 (with salt/username)", "110", None, 70))
|
||||||
|
|
||||||
|
# PostgreSQL md5
|
||||||
|
if hash_string.startswith('md5') and len(hash_string) == 35:
|
||||||
|
possible_types.append(("PostgreSQL MD5", "3100", "postgres", 90))
|
||||||
|
|
||||||
|
return possible_types if possible_types else None
|
||||||
|
|
||||||
|
# MySQL formats
|
||||||
|
if hash_string.startswith('*') and length == 41 and re.match(r'^\*[A-F0-9]{40}$', hash_string.upper()):
|
||||||
|
possible_types.append(("MySQL 4.1/5.x", "300", "mysql-sha1", 95))
|
||||||
|
return possible_types
|
||||||
|
|
||||||
|
# Oracle formats
|
||||||
|
if re.match(r'^[A-F0-9]{16}:[A-F0-9]{16}$', hash_string.upper()):
|
||||||
|
possible_types.append(("Oracle 11g", "112", "oracle11", 90))
|
||||||
|
return possible_types
|
||||||
|
|
||||||
|
if re.match(r'^S:[A-F0-9]{60}$', hash_string.upper()):
|
||||||
|
possible_types.append(("Oracle 12c/18c", "12300", "oracle12c", 90))
|
||||||
|
return possible_types
|
||||||
|
|
||||||
|
# MSSQL formats
|
||||||
|
if re.match(r'^0x0100[A-F0-9]{8}[A-F0-9]{40}$', hash_string.upper()):
|
||||||
|
possible_types.append(("MSSQL 2000", "131", "mssql", 90))
|
||||||
|
return possible_types
|
||||||
|
|
||||||
|
if re.match(r'^0x0200[A-F0-9]{8}[A-F0-9]{128}$', hash_string.upper()):
|
||||||
|
possible_types.append(("MSSQL 2012/2014", "1731", "mssql12", 90))
|
||||||
|
return possible_types
|
||||||
|
|
||||||
|
# Base64 pattern check
|
||||||
|
is_base64 = re.match(r'^[A-Za-z0-9+/]+=*$', hash_string) and length % 4 == 0
|
||||||
|
|
||||||
|
# Raw hash identification by length
|
||||||
|
is_hex = re.match(r'^[a-f0-9]+$', hash_lower)
|
||||||
|
|
||||||
|
if is_hex:
|
||||||
|
if length == 16:
|
||||||
|
possible_types.append(("MySQL < 4.1", "200", "mysql", 85))
|
||||||
|
possible_types.append(("Half MD5", None, None, 60))
|
||||||
|
|
||||||
|
elif length == 32:
|
||||||
|
possible_types.append(("MD5", "0", "raw-md5", 80))
|
||||||
|
possible_types.append(("MD4", "900", "raw-md4", 70))
|
||||||
|
possible_types.append(("NTLM", "1000", "nt", 75))
|
||||||
|
possible_types.append(("LM", "3000", "lm", 60))
|
||||||
|
possible_types.append(("RAdmin v2.x", "9900", None, 50))
|
||||||
|
possible_types.append(("Snefru-128", None, None, 40))
|
||||||
|
possible_types.append(("HMAC-MD5 (key = $pass)", "50", None, 50))
|
||||||
|
|
||||||
|
elif length == 40:
|
||||||
|
possible_types.append(("SHA-1", "100", "raw-sha1", 85))
|
||||||
|
possible_types.append(("RIPEMD-160", "6000", "ripemd-160", 65))
|
||||||
|
possible_types.append(("Tiger-160", None, None, 50))
|
||||||
|
possible_types.append(("Haval-160", None, None, 45))
|
||||||
|
possible_types.append(("HMAC-SHA1 (key = $pass)", "150", None, 55))
|
||||||
|
|
||||||
|
elif length == 48:
|
||||||
|
possible_types.append(("Tiger-192", None, None, 70))
|
||||||
|
possible_types.append(("Haval-192", None, None, 65))
|
||||||
|
|
||||||
|
elif length == 56:
|
||||||
|
possible_types.append(("SHA-224", "1300", "raw-sha224", 85))
|
||||||
|
possible_types.append(("Haval-224", None, None, 60))
|
||||||
|
|
||||||
|
elif length == 64:
|
||||||
|
possible_types.append(("SHA-256", "1400", "raw-sha256", 85))
|
||||||
|
possible_types.append(("RIPEMD-256", None, None, 60))
|
||||||
|
possible_types.append(("SHA3-256", "17400", "raw-sha3", 70))
|
||||||
|
possible_types.append(("Keccak-256", "17800", "raw-keccak-256", 70))
|
||||||
|
possible_types.append(("Haval-256", None, None, 50))
|
||||||
|
possible_types.append(("GOST R 34.11-94", "6900", None, 55))
|
||||||
|
possible_types.append(("BLAKE2b-256", None, None, 60))
|
||||||
|
|
||||||
|
elif length == 80:
|
||||||
|
possible_types.append(("RIPEMD-320", None, None, 80))
|
||||||
|
|
||||||
|
elif length == 96:
|
||||||
|
possible_types.append(("SHA-384", "10800", "raw-sha384", 85))
|
||||||
|
possible_types.append(("SHA3-384", "17900", None, 70))
|
||||||
|
possible_types.append(("Keccak-384", None, None, 65))
|
||||||
|
|
||||||
|
elif length == 128:
|
||||||
|
possible_types.append(("SHA-512", "1700", "raw-sha512", 85))
|
||||||
|
possible_types.append(("Whirlpool", "6100", "whirlpool", 75))
|
||||||
|
possible_types.append(("SHA3-512", "17600", None, 70))
|
||||||
|
possible_types.append(("Keccak-512", None, None, 65))
|
||||||
|
possible_types.append(("BLAKE2b-512", None, None, 60))
|
||||||
|
|
||||||
|
# Base64 encoded hashes
|
||||||
|
elif is_base64:
|
||||||
|
if length == 24:
|
||||||
|
possible_types.append(("MD5 (Base64)", None, None, 75))
|
||||||
|
elif length == 28:
|
||||||
|
possible_types.append(("SHA-1 (Base64)", None, None, 75))
|
||||||
|
elif length == 32:
|
||||||
|
possible_types.append(("SHA-224 (Base64)", None, None, 75))
|
||||||
|
elif length == 44:
|
||||||
|
possible_types.append(("SHA-256 (Base64)", None, None, 75))
|
||||||
|
elif length == 64:
|
||||||
|
possible_types.append(("SHA-384 (Base64)", None, None, 75))
|
||||||
|
elif length == 88:
|
||||||
|
possible_types.append(("SHA-512 (Base64)", None, None, 75))
|
||||||
|
|
||||||
|
return possible_types if possible_types else [("Unknown", None, None, 0)]
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle the !hashid command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("hashid"):
|
||||||
|
logging.info("Received !hashid command")
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
if len(args) < 1:
|
||||||
|
usage_msg = """<strong>🔐 Hash Identifier Usage</strong>
|
||||||
|
|
||||||
|
<strong>Usage:</strong> <code>!hashid <hash></code>
|
||||||
|
|
||||||
|
<strong>Examples:</strong>
|
||||||
|
• <code>!hashid 5f4dcc3b5aa765d61d8327deb882cf99</code>
|
||||||
|
• <code>!hashid 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8</code>
|
||||||
|
• <code>!hashid $6$rounds=5000$salt$hash...</code>
|
||||||
|
• <code>!hashid $y$j9T$...</code> (yescrypt from /etc/shadow)
|
||||||
|
|
||||||
|
<strong>Supported Hash Types:</strong>
|
||||||
|
• <strong>Modern:</strong> yescrypt, scrypt, Argon2, bcrypt
|
||||||
|
• <strong>Unix Crypt:</strong> SHA-512 Crypt, SHA-256 Crypt, MD5 Crypt
|
||||||
|
• <strong>Raw Hashes:</strong> MD5, SHA-1/224/256/384/512, SHA-3, NTLM, LM
|
||||||
|
• <strong>Database:</strong> MySQL, PostgreSQL, Oracle, MSSQL
|
||||||
|
• <strong>CMS:</strong> Wordpress, phpBB3, Drupal, Django
|
||||||
|
• <strong>LDAP:</strong> SSHA, SMD5, and various LDAP formats
|
||||||
|
• <strong>Network:</strong> NetNTLMv1/v2, Kerberos
|
||||||
|
• <strong>Exotic:</strong> Whirlpool, RIPEMD, BLAKE2, Keccak, GOST
|
||||||
|
"""
|
||||||
|
await bot.api.send_markdown_message(room.room_id, usage_msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
hash_input = ' '.join(args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Identify the hash
|
||||||
|
identified = identify_hash(hash_input)
|
||||||
|
|
||||||
|
if not identified:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Could not identify hash type. Please verify the hash format."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sort by confidence (highest first)
|
||||||
|
identified = sorted(identified, key=lambda x: x[3], reverse=True)
|
||||||
|
|
||||||
|
# Format the response
|
||||||
|
hash_preview = hash_input[:60] + "..." if len(hash_input) > 60 else hash_input
|
||||||
|
|
||||||
|
# Determine confidence indicator
|
||||||
|
top_confidence = identified[0][3]
|
||||||
|
if top_confidence >= 90:
|
||||||
|
confidence_emoji = "🟢"
|
||||||
|
confidence_label = "Very High"
|
||||||
|
elif top_confidence >= 80:
|
||||||
|
confidence_emoji = "🟡"
|
||||||
|
confidence_label = "High"
|
||||||
|
elif top_confidence >= 60:
|
||||||
|
confidence_emoji = "🟠"
|
||||||
|
confidence_label = "Medium"
|
||||||
|
else:
|
||||||
|
confidence_emoji = "🔴"
|
||||||
|
confidence_label = "Low"
|
||||||
|
|
||||||
|
# Build response inside collapsible details
|
||||||
|
response = "<details><summary><strong>🔐 Hash Identification Results</strong></summary>\n"
|
||||||
|
response += "<br>\n"
|
||||||
|
response += f"<strong>Input:</strong> <code>{hash_preview}</code><br>\n"
|
||||||
|
response += f"<strong>Length:</strong> {len(hash_input)} characters<br>\n"
|
||||||
|
response += f"<strong>Overall Confidence:</strong> {confidence_emoji} {confidence_label} ({top_confidence}%)<br>\n"
|
||||||
|
response += "<br>\n"
|
||||||
|
|
||||||
|
response += f"<strong>Possible Hash Types ({len(identified)}):</strong><br>\n"
|
||||||
|
|
||||||
|
for idx, (hash_type, hashcat_mode, john_format, confidence) in enumerate(identified, 1):
|
||||||
|
# Confidence indicator per hash
|
||||||
|
if confidence >= 90:
|
||||||
|
conf_emoji = "🟢"
|
||||||
|
elif confidence >= 80:
|
||||||
|
conf_emoji = "🟡"
|
||||||
|
elif confidence >= 60:
|
||||||
|
conf_emoji = "🟠"
|
||||||
|
else:
|
||||||
|
conf_emoji = "🔴"
|
||||||
|
|
||||||
|
response += f" <strong>{idx}. {hash_type}</strong> {conf_emoji} {confidence}%<br>\n"
|
||||||
|
|
||||||
|
tools = []
|
||||||
|
if hashcat_mode:
|
||||||
|
tools.append(f"Hashcat: <code>-m {hashcat_mode}</code>")
|
||||||
|
if john_format:
|
||||||
|
tools.append(f"John: <code>--format={john_format}</code>")
|
||||||
|
|
||||||
|
if tools:
|
||||||
|
response += f" {' | '.join(tools)}<br>\n"
|
||||||
|
|
||||||
|
response += "<br>\n"
|
||||||
|
|
||||||
|
# Add useful tips
|
||||||
|
if len(identified) == 1 and identified[0][0] not in ["Unknown", "Unknown Modular Crypt Format"]:
|
||||||
|
response += "<br><strong>💡 Single match with high confidence</strong><br>\n"
|
||||||
|
elif len(identified) > 5:
|
||||||
|
response += "<br><em>ℹ️ Multiple possibilities - context may help narrow it down</em><br>\n"
|
||||||
|
|
||||||
|
# Add legend
|
||||||
|
response += "<br>\n"
|
||||||
|
response += "<strong>Confidence Legend:</strong><br>\n"
|
||||||
|
response += "🟢 Very High (90-100%) | 🟡 High (80-89%) | 🟠 Medium (60-79%) | 🔴 Low (0-59%)<br>\n"
|
||||||
|
|
||||||
|
response += "</details>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, response)
|
||||||
|
logging.info(f"Identified hash types: {', '.join([f'{h[0]} ({h[3]}%)' for h in identified])}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Error identifying hash: {str(e)}"
|
||||||
|
)
|
||||||
|
logging.error(f"Error in hashid command: {e}", exc_info=True)
|
387
plugins/headers.py
Normal file
387
plugins/headers.py
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides comprehensive HTTP security header analysis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import ssl
|
||||||
|
import socket
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle !headers command for HTTP security header analysis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("headers"):
|
||||||
|
logging.info("Received !headers command")
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
if len(args) < 1:
|
||||||
|
await show_usage(room, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
url = args[0].strip()
|
||||||
|
|
||||||
|
# Add protocol if missing
|
||||||
|
if not url.startswith(('http://', 'https://')):
|
||||||
|
url = 'https://' + url
|
||||||
|
|
||||||
|
await analyze_headers(room, bot, url)
|
||||||
|
|
||||||
|
async def show_usage(room, bot):
|
||||||
|
"""Display headers command usage."""
|
||||||
|
usage = """
|
||||||
|
<strong>🔒 HTTP Security Headers Analysis</strong>
|
||||||
|
|
||||||
|
<strong>!headers <url></strong> - Comprehensive HTTP security header analysis
|
||||||
|
|
||||||
|
<strong>Examples:</strong>
|
||||||
|
• <code>!headers example.com</code>
|
||||||
|
• <code>!headers https://github.com</code>
|
||||||
|
• <code>!headers http://localhost:8080</code>
|
||||||
|
|
||||||
|
<strong>Analyzes:</strong>
|
||||||
|
• Security headers presence and configuration
|
||||||
|
• SSL/TLS certificate information
|
||||||
|
• HTTP to HTTPS redirects
|
||||||
|
• Security scoring and recommendations
|
||||||
|
"""
|
||||||
|
await bot.api.send_markdown_message(room.room_id, usage)
|
||||||
|
|
||||||
|
async def analyze_headers(room, bot, url):
|
||||||
|
"""Perform comprehensive HTTP security header analysis."""
|
||||||
|
try:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"🔍 Analyzing security headers for: {url}")
|
||||||
|
|
||||||
|
results = {
|
||||||
|
'url': url,
|
||||||
|
'http_headers': {},
|
||||||
|
'https_headers': {},
|
||||||
|
'redirect_chain': [],
|
||||||
|
'ssl_info': {},
|
||||||
|
'security_score': 0,
|
||||||
|
'recommendations': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test HTTP first (if HTTPS was provided, we'll still check redirects)
|
||||||
|
parsed = urlparse(url)
|
||||||
|
http_url = f"http://{parsed.netloc or parsed.path}"
|
||||||
|
https_url = f"https://{parsed.netloc or parsed.path}"
|
||||||
|
|
||||||
|
# Analyze HTTP response and redirects
|
||||||
|
await analyze_http_response(results, http_url if not url.startswith('https://') else https_url)
|
||||||
|
|
||||||
|
# Analyze HTTPS response
|
||||||
|
if url.startswith('https://') or results.get('redirects_to_https'):
|
||||||
|
await analyze_https_response(results, https_url)
|
||||||
|
|
||||||
|
# Analyze SSL certificate if HTTPS
|
||||||
|
if url.startswith('https://') or results.get('redirects_to_https'):
|
||||||
|
await analyze_ssl_certificate(results, parsed.netloc or parsed.path)
|
||||||
|
|
||||||
|
# Calculate security score
|
||||||
|
await calculate_security_score(results)
|
||||||
|
|
||||||
|
# Generate recommendations
|
||||||
|
await generate_recommendations(results)
|
||||||
|
|
||||||
|
# Format and send results
|
||||||
|
output = await format_header_analysis(results)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
|
||||||
|
logging.info(f"Completed header analysis for {url}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error analyzing headers: {str(e)}")
|
||||||
|
logging.error(f"Error in analyze_headers: {e}")
|
||||||
|
|
||||||
|
async def analyze_http_response(results, url):
|
||||||
|
"""Analyze HTTP response and redirect chain."""
|
||||||
|
try:
|
||||||
|
session = requests.Session()
|
||||||
|
session.max_redirects = 5
|
||||||
|
|
||||||
|
response = session.get(url, timeout=10, allow_redirects=True)
|
||||||
|
results['final_url'] = response.url
|
||||||
|
results['status_code'] = response.status_code
|
||||||
|
results['http_headers'] = dict(response.headers)
|
||||||
|
|
||||||
|
# Check if redirects to HTTPS
|
||||||
|
results['redirects_to_https'] = response.url.startswith('https://')
|
||||||
|
|
||||||
|
# Store redirect history
|
||||||
|
results['redirect_chain'] = [{
|
||||||
|
'url': resp.url,
|
||||||
|
'status_code': resp.status_code,
|
||||||
|
'headers': dict(resp.headers)
|
||||||
|
} for resp in response.history]
|
||||||
|
|
||||||
|
except requests.exceptions.SSLError:
|
||||||
|
results['ssl_error'] = True
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
results['http_error'] = str(e)
|
||||||
|
|
||||||
|
async def analyze_https_response(results, url):
|
||||||
|
"""Analyze HTTPS response headers."""
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=10, allow_redirects=False)
|
||||||
|
results['https_headers'] = dict(response.headers)
|
||||||
|
results['https_status'] = response.status_code
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
results['https_error'] = str(e)
|
||||||
|
|
||||||
|
async def analyze_ssl_certificate(results, domain):
|
||||||
|
"""Analyze SSL certificate information."""
|
||||||
|
try:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
with socket.create_connection((domain, 443), timeout=10) as sock:
|
||||||
|
with context.wrap_socket(sock, server_hostname=domain) as ssock:
|
||||||
|
cert = ssock.getpeercert()
|
||||||
|
|
||||||
|
results['ssl_info'] = {
|
||||||
|
'subject': dict(x[0] for x in cert['subject']),
|
||||||
|
'issuer': dict(x[0] for x in cert['issuer']),
|
||||||
|
'not_before': cert['notBefore'],
|
||||||
|
'not_after': cert['notAfter'],
|
||||||
|
'san': cert.get('subjectAltName', []),
|
||||||
|
'version': cert.get('version'),
|
||||||
|
'serial_number': cert.get('serialNumber')
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results['ssl_error'] = str(e)
|
||||||
|
|
||||||
|
async def calculate_security_score(results):
|
||||||
|
"""Calculate overall security score based on headers and configuration."""
|
||||||
|
score = 100
|
||||||
|
missing_headers = []
|
||||||
|
|
||||||
|
# Critical security headers
|
||||||
|
critical_headers = [
|
||||||
|
'Strict-Transport-Security',
|
||||||
|
'Content-Security-Policy',
|
||||||
|
'X-Content-Type-Options',
|
||||||
|
'X-Frame-Options',
|
||||||
|
'X-XSS-Protection'
|
||||||
|
]
|
||||||
|
|
||||||
|
headers = results.get('https_headers') or results.get('http_headers', {})
|
||||||
|
|
||||||
|
for header in critical_headers:
|
||||||
|
if header not in headers:
|
||||||
|
score -= 15
|
||||||
|
missing_headers.append(header)
|
||||||
|
|
||||||
|
# Check HSTS configuration
|
||||||
|
hsts = headers.get('Strict-Transport-Security', '')
|
||||||
|
if 'max-age=31536000' not in hsts:
|
||||||
|
score -= 10
|
||||||
|
if 'includeSubDomains' not in hsts:
|
||||||
|
score -= 5
|
||||||
|
if 'preload' not in hsts:
|
||||||
|
score -= 5
|
||||||
|
|
||||||
|
# Check CSP configuration
|
||||||
|
csp = headers.get('Content-Security-Policy', '')
|
||||||
|
if not csp:
|
||||||
|
score -= 10
|
||||||
|
elif "default-src 'none'" not in csp and "default-src 'self'" not in csp:
|
||||||
|
score -= 5
|
||||||
|
|
||||||
|
# Check for insecure headers
|
||||||
|
insecure_headers = ['Server', 'X-Powered-By', 'X-AspNet-Version']
|
||||||
|
for header in insecure_headers:
|
||||||
|
if header in headers:
|
||||||
|
score -= 5
|
||||||
|
|
||||||
|
# Bonus for good practices
|
||||||
|
if headers.get('Referrer-Policy'):
|
||||||
|
score += 5
|
||||||
|
if headers.get('Feature-Policy') or headers.get('Permissions-Policy'):
|
||||||
|
score += 5
|
||||||
|
if headers.get('X-Content-Type-Options') == 'nosniff':
|
||||||
|
score += 5
|
||||||
|
if headers.get('X-Frame-Options') in ['DENY', 'SAMEORIGIN']:
|
||||||
|
score += 5
|
||||||
|
|
||||||
|
# HTTPS enforcement bonus
|
||||||
|
if results.get('redirects_to_https'):
|
||||||
|
score += 10
|
||||||
|
|
||||||
|
results['security_score'] = max(0, score)
|
||||||
|
results['missing_headers'] = missing_headers
|
||||||
|
|
||||||
|
async def generate_recommendations(results):
|
||||||
|
"""Generate security recommendations based on analysis."""
|
||||||
|
recommendations = []
|
||||||
|
headers = results.get('https_headers') or results.get('http_headers', {})
|
||||||
|
|
||||||
|
# HSTS recommendations
|
||||||
|
if 'Strict-Transport-Security' not in headers:
|
||||||
|
recommendations.append("🔒 Implement HSTS header with max-age=31536000, includeSubDomains, and preload")
|
||||||
|
else:
|
||||||
|
hsts = headers['Strict-Transport-Security']
|
||||||
|
if 'max-age=31536000' not in hsts:
|
||||||
|
recommendations.append("🔒 Increase HSTS max-age to 31536000 (1 year)")
|
||||||
|
if 'includeSubDomains' not in hsts:
|
||||||
|
recommendations.append("🔒 Add includeSubDomains to HSTS header")
|
||||||
|
if 'preload' not in hsts:
|
||||||
|
recommendations.append("🔒 Consider adding preload directive to HSTS for browser preloading")
|
||||||
|
|
||||||
|
# CSP recommendations
|
||||||
|
if 'Content-Security-Policy' not in headers:
|
||||||
|
recommendations.append("🛡️ Implement Content Security Policy to prevent XSS attacks")
|
||||||
|
else:
|
||||||
|
csp = headers['Content-Security-Policy']
|
||||||
|
if "default-src 'self'" not in csp and "default-src 'none'" not in csp:
|
||||||
|
recommendations.append("🛡️ Restrict CSP default-src to 'self' or specific origins")
|
||||||
|
|
||||||
|
# Frame options
|
||||||
|
if 'X-Frame-Options' not in headers:
|
||||||
|
recommendations.append("🚫 Add X-Frame-Options header to prevent clickjacking (DENY or SAMEORIGIN)")
|
||||||
|
|
||||||
|
# Content type options
|
||||||
|
if 'X-Content-Type-Options' not in headers:
|
||||||
|
recommendations.append("📄 Add X-Content-Type-Options: nosniff to prevent MIME type sniffing")
|
||||||
|
|
||||||
|
# Referrer policy
|
||||||
|
if 'Referrer-Policy' not in headers:
|
||||||
|
recommendations.append("🔗 Implement Referrer-Policy to control referrer information leakage")
|
||||||
|
|
||||||
|
# Feature policy
|
||||||
|
if 'Feature-Policy' not in headers and 'Permissions-Policy' not in headers:
|
||||||
|
recommendations.append("⚙️ Implement Feature-Policy/Permissions-Policy to restrict browser features")
|
||||||
|
|
||||||
|
# Remove server information
|
||||||
|
if 'Server' in headers or 'X-Powered-By' in headers:
|
||||||
|
recommendations.append("🕵️ Remove Server and X-Powered-By headers to avoid information disclosure")
|
||||||
|
|
||||||
|
# HTTPS enforcement
|
||||||
|
if not results.get('redirects_to_https') and not results['url'].startswith('https://'):
|
||||||
|
recommendations.append("🔐 Implement HTTP to HTTPS redirects")
|
||||||
|
|
||||||
|
results['recommendations'] = recommendations
|
||||||
|
|
||||||
|
async def format_header_analysis(results):
|
||||||
|
"""Format the header analysis results for display."""
|
||||||
|
output = f"<strong>🔒 Security Headers Analysis: {results['url']}</strong><br><br>"
|
||||||
|
|
||||||
|
# Security Score
|
||||||
|
score = results['security_score']
|
||||||
|
score_emoji = "🟢" if score >= 80 else "🟡" if score >= 60 else "🔴"
|
||||||
|
output += f"<strong>{score_emoji} Security Score: {score}/100</strong><br><br>"
|
||||||
|
|
||||||
|
# Basic Information
|
||||||
|
output += "<strong>📊 Basic Information</strong><br>"
|
||||||
|
output += f" • <strong>Final URL:</strong> {results.get('final_url', 'N/A')}<br>"
|
||||||
|
output += f" • <strong>Status Code:</strong> {results.get('status_code', 'N/A')}<br>"
|
||||||
|
if results.get('redirects_to_https'):
|
||||||
|
output += f" • <strong>HTTPS Redirect:</strong> ✅ Enforced<br>"
|
||||||
|
else:
|
||||||
|
output += f" • <strong>HTTPS Redirect:</strong> ❌ Not enforced<br>"
|
||||||
|
output += f" • <strong>Redirect Chain:</strong> {len(results.get('redirect_chain', []))} hops<br>"
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Security Headers Analysis
|
||||||
|
headers = results.get('https_headers') or results.get('http_headers', {})
|
||||||
|
output += "<strong>🛡️ Security Headers Analysis</strong><br>"
|
||||||
|
|
||||||
|
security_headers = {
|
||||||
|
'Strict-Transport-Security': ('🔒', 'HSTS - HTTP Strict Transport Security'),
|
||||||
|
'Content-Security-Policy': ('🛡️', 'CSP - Content Security Policy'),
|
||||||
|
'X-Frame-Options': ('🚫', 'Clickjacking Protection'),
|
||||||
|
'X-Content-Type-Options': ('📄', 'MIME Type Sniffing Protection'),
|
||||||
|
'X-XSS-Protection': ('❌', 'XSS Protection (Deprecated)'),
|
||||||
|
'Referrer-Policy': ('🔗', 'Referrer Policy'),
|
||||||
|
'Feature-Policy': ('⚙️', 'Feature Policy'),
|
||||||
|
'Permissions-Policy': ('🔧', 'Permissions Policy'),
|
||||||
|
}
|
||||||
|
|
||||||
|
for header, (emoji, description) in security_headers.items():
|
||||||
|
if header in headers:
|
||||||
|
value = headers[header]
|
||||||
|
if len(value) > 100:
|
||||||
|
value = value[:100] + "..."
|
||||||
|
output += f" • {emoji} <strong>{header}:</strong> ✅ {value}<br>"
|
||||||
|
else:
|
||||||
|
output += f" • {emoji} <strong>{header}:</strong> ❌ Missing<br>"
|
||||||
|
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Other Headers (Information Disclosure)
|
||||||
|
output += "<strong>📋 Other Headers</strong><br>"
|
||||||
|
info_headers = ['Server', 'X-Powered-By', 'X-AspNet-Version']
|
||||||
|
for header in info_headers:
|
||||||
|
if header in headers:
|
||||||
|
output += f" • 🔍 <strong>{header}:</strong> {headers[header]}<br>"
|
||||||
|
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# SSL Certificate Information (if available)
|
||||||
|
if results.get('ssl_info'):
|
||||||
|
output += "<strong>🔐 SSL Certificate</strong><br>"
|
||||||
|
ssl_info = results['ssl_info']
|
||||||
|
if ssl_info.get('subject'):
|
||||||
|
output += f" • <strong>Subject:</strong> {ssl_info['subject'].get('commonName', 'N/A')}<br>"
|
||||||
|
if ssl_info.get('issuer'):
|
||||||
|
output += f" • <strong>Issuer:</strong> {ssl_info['issuer'].get('organizationName', 'N/A')}<br>"
|
||||||
|
if ssl_info.get('not_after'):
|
||||||
|
output += f" • <strong>Expires:</strong> {ssl_info['not_after']}<br>"
|
||||||
|
if ssl_info.get('san'):
|
||||||
|
san_count = len([san for san in ssl_info['san'] if san[0] == 'DNS'])
|
||||||
|
output += f" • <strong>SAN Entries:</strong> {san_count}<br>"
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Recommendations
|
||||||
|
if results.get('recommendations'):
|
||||||
|
output += "<strong>💡 Security Recommendations</strong><br>"
|
||||||
|
for rec in results['recommendations'][:8]: # Show first 8 recommendations
|
||||||
|
output += f" • {rec}<br>"
|
||||||
|
|
||||||
|
if len(results['recommendations']) > 8:
|
||||||
|
output += f" • ... and {len(results['recommendations']) - 8} more recommendations<br>"
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Missing Headers Summary
|
||||||
|
if results.get('missing_headers'):
|
||||||
|
output += "<strong>⚠️ Critical Headers Missing</strong><br>"
|
||||||
|
for header in results['missing_headers']:
|
||||||
|
output += f" • ❌ {header}<br>"
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Security Rating
|
||||||
|
score = results['security_score']
|
||||||
|
if score >= 80:
|
||||||
|
rating = "🟢 Excellent"
|
||||||
|
description = "Strong security headers configuration"
|
||||||
|
elif score >= 60:
|
||||||
|
rating = "🟡 Good"
|
||||||
|
description = "Moderate security, room for improvement"
|
||||||
|
elif score >= 40:
|
||||||
|
rating = "🟠 Fair"
|
||||||
|
description = "Basic security, significant improvements needed"
|
||||||
|
else:
|
||||||
|
rating = "🔴 Poor"
|
||||||
|
description = "Weak security headers configuration"
|
||||||
|
|
||||||
|
output += f"<strong>📈 Security Rating:</strong> {rating}<br>"
|
||||||
|
output += f"<strong>📝 Assessment:</strong> {description}<br>"
|
||||||
|
|
||||||
|
# Wrap in collapsible if content is large
|
||||||
|
if len(output) > 1000:
|
||||||
|
output = f"<details><summary><strong>🔒 Security Headers Analysis: {results['url']}</strong></summary>{output}</details>"
|
||||||
|
|
||||||
|
return output
|
324
plugins/help.py
324
plugins/help.py
@@ -1,72 +1,356 @@
|
|||||||
"""
|
"""
|
||||||
This plugin provides a command to display the list of available commands and their descriptions.
|
Plugin for providing a command to display the list of available commands and their descriptions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# plugins/help.py
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import simplematrixbotlib as botlib
|
import simplematrixbotlib as botlib
|
||||||
|
|
||||||
async def handle_command(room, message, bot, prefix, config):
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
"""
|
"""
|
||||||
Function to handle the !help command.
|
Function to handle the !help command.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
room (Room): The Matrix room where the command was invoked.
|
room (Room): The Matrix room where the command was invoked.
|
||||||
message (RoomMessage): The message object containing the command.
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (MatrixBot): The Matrix bot instance.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): The bot's configuration.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("help"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("help"):
|
||||||
logging.info("Fetching command help documentation")
|
logging.info("Fetching command help documentation")
|
||||||
commands_message = """
|
commands_message = """
|
||||||
<details><summary><strong>🍄Funguy Bot Commands🍄<br>⤵︎Click Here To See Help Text⤵︎</strong></summary>
|
<details><summary><strong>🍄 Funguy Bot Commands 🍄</strong></summary>
|
||||||
<p>
|
<p>
|
||||||
|
<details><summary>📖 <strong>!help</strong></summary>
|
||||||
|
<p>Displays comprehensive help documentation for all available commands with usage examples.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🔌 <strong>!plugins</strong></summary>
|
||||||
|
<p>Lists all loaded plugins along with their descriptions in alphabetical order.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details><summary>🃏 <strong>!fortune</strong></summary>
|
<details><summary>🃏 <strong>!fortune</strong></summary>
|
||||||
<p>Returns a random fortune message. Executes the `/usr/games/fortune` utility and sends the output as a message to the chat room.</p>
|
<p>Returns a random fortune message. Executes the `/usr/games/fortune` utility and sends the output as a message to the chat room.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>⏰ <strong>!date</strong></summary>
|
<details><summary>⏰ <strong>!date</strong></summary>
|
||||||
<p>Displays the current date and time. Fetches the current date and time using Python's `datetime` module and sends it in a formatted message to the chat room.</p>
|
<p>Displays the current date and time. Fetches the current date and time using Python's `datetime` module and sends it in a formatted message with proper ordinal suffixes to the chat room.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>💻 <strong>!proxy</strong></summary>
|
<details><summary>💻 <strong>!proxy</strong></summary>
|
||||||
<p>Retrieves a tested/working random SOCKS5 proxy. Fetches a list of SOCKS5 proxies, tests their availability, and sends the first working proxy to the chat room.</p>
|
<p>Retrieves a tested/working random SOCKS5 proxy. Fetches a list of SOCKS5 proxies from public sources, tests their availability, and sends the first working proxy with latency information to the chat room. Caches working proxies for faster access.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>📶 <strong>!isup [domain/ip]</strong></summary>
|
<details><summary>📶 <strong>!isup [domain/ip]</strong></summary>
|
||||||
<p>Checks if the specified domain or IP address is reachable. Checks if the specified domain or IP address is reachable by attempting to ping it. If DNS resolution is successful, it checks HTTP and HTTPS service availability by sending requests to the domain.</p>
|
<p>Checks if the specified domain or IP address is reachable. Performs DNS resolution and checks HTTP/HTTPS service availability. Reports successful DNS resolution and service status.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>☯ <strong>!karma [user]</strong></summary>
|
<details><summary>☯ <strong>!karma [user]</strong></summary>
|
||||||
<p>Retrieves the karma points for the specified user. Retrieves the karma points for the specified user from a database and sends them as a message to the chat room.</p>
|
<p>Retrieves the karma points for the specified user. Retrieves the karma points for the specified user from a SQLite database and sends them as a message to the chat room.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>⇧ <strong>!karma [user] up</strong></summary>
|
<details><summary>⇧ <strong>!karma [user] up</strong></summary>
|
||||||
<p>Increases the karma points for the specified user by 1. Increases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room.</p>
|
<p>Increases the karma points for the specified user by 1. Increases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room. Users cannot modify their own karma.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>⇩ <strong>!karma [user] down</strong></summary>
|
<details><summary>⇩ <strong>!karma [user] down</strong></summary>
|
||||||
<p>Decreases the karma points for the specified user by 1. Decreases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room.</p>
|
<p>Decreases the karma points for the specified user by 1. Decreases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room. Users cannot modify their own karma.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>📄 <strong>!funguy [prompt]</strong></summary>
|
<details><summary>🌧️ <strong>!weather [location]</strong></summary>
|
||||||
<p>An AI large language model designed to serve as a chatbot within a vibrant and diverse community chat room hosted on the Matrix platform. (Currently loaded model: <strong>TheBloke_Mistral-7B-Instruct-v0.2-GPTQ</strong>)</p>
|
<p>Fetches current weather information for any location using OpenWeatherMap API. Shows temperature (Celsius/Fahrenheit), conditions, humidity, wind speed, and weather emojis. Requires OPENWEATHER_API_KEY environment variable.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>🎝 <strong>!music [prompt]</strong></summary>
|
<details><summary>📖 <strong>!ud [term] [index]</strong></summary>
|
||||||
<p>Your music expert! Try it out.</p>
|
<p>Fetches definitions from Urban Dictionary. Use without arguments for random definition, or specify term and optional index number. Shows definition, example, author, votes, and permalink.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary>🌟 <strong>Funguy Bot Credits</strong> 🌟</summary>
|
<details><summary>🔍 <strong>!dns [domain]</strong></summary>
|
||||||
<p>🧙♂️ Creator & Developer: Hash Borgir is the author of 🍄Funguy Bot🍄. (@hashborgir:mozilla.org)</p>
|
<p>Performs comprehensive DNS reconnaissance on a domain. Queries multiple DNS record types including A, AAAA, MX, NS, TXT, CNAME, SOA, and SRV records. Validates domain format and provides formatted results.</p>
|
||||||
<p>Join our Matrix Room: [Self-hosting | Security | Sysadmin | Homelab | Programming](https://matrix.to/#/#selfhosting:mozilla.org)</p>
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details><summary>💰 <strong>!btc</strong></summary>
|
||||||
|
<p>Fetches the current Bitcoin price in USD from bitcointicker.co API. Shows real-time BTC/USD price with proper formatting. Includes error handling for API timeouts and data parsing issues.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🔍 <strong>!shodan [command] [query]</strong></summary>
|
||||||
|
<p>Shodan.io integration for security reconnaissance and threat intelligence.</p>
|
||||||
|
<p><strong>Commands:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!shodan ip <ip_address></code> - Detailed IP information (services, ports, banners)</li>
|
||||||
|
<li><code>!shodan search <query></code> - Search Shodan database with filters</li>
|
||||||
|
<li><code>!shodan host <domain></code> - Host information and subdomain enumeration</li>
|
||||||
|
<li><code>!shodan count <query></code> - Count results with geographic/organization breakdown</li>
|
||||||
|
<li><code>!shodan test</code> - Test API connection and debug queries</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Search Examples:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!shodan search apache</code></li>
|
||||||
|
<li><code>!shodan search "port:22 country:US"</code></li>
|
||||||
|
<li><code>!shodan search "product:nginx"</code></li>
|
||||||
|
<li><code>!shodan search "net:192.168.1.0/24"</code></li>
|
||||||
|
<li><code>!shodan search "http.title:'admin'"</code></li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Common Filters:</strong> country, city, port, product, os, org, net, has_ssl, http.title</p>
|
||||||
|
<p><em>Requires SHODAN_KEY environment variable</em></p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🌐 <strong>!dnsdumpster [domain]</strong></summary>
|
||||||
|
<p>Comprehensive DNS reconnaissance and attack surface mapping using DNSDumpster.com API.</p>
|
||||||
|
<p><strong>Commands:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!dnsdumpster <domain></code> - Complete DNS reconnaissance for any domain</li>
|
||||||
|
<li><code>!dnsdumpster test</code> - Test API connection and key validity</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Features:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>A Records - All IPv4 addresses with geographic and ASN information</li>
|
||||||
|
<li>NS Records - Complete name server information with IP locations</li>
|
||||||
|
<li>MX Records - All mail servers with geographic data</li>
|
||||||
|
<li>CNAME Records - Full alias chain mappings</li>
|
||||||
|
<li>TXT Records - All text records including SPF, DKIM, verification</li>
|
||||||
|
<li>Additional Records - AAAA, SRV, SOA, PTR records when available</li>
|
||||||
|
<li>Web Services - HTTP/HTTPS service detection with banner information</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Examples:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!dnsdumpster google.com</code></li>
|
||||||
|
<li><code>!dnsdumpster github.com</code></li>
|
||||||
|
<li><code>!dnsdumpster example.com</code></li>
|
||||||
|
</ul>
|
||||||
|
<p><em>Requires DNSDUMPSTER_KEY environment variable</em><br>
|
||||||
|
<em>Rate Limit: 1 request per 2 seconds</em></p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>💣 !exploitdb - Search Exploit Database</strong></summary>
|
||||||
|
<br>
|
||||||
|
<strong>Description:</strong><br>
|
||||||
|
Search Exploit-DB for security vulnerabilities and exploits. Returns detailed information about exploits including EDB-ID, type, platform, author, and direct links to exploit code.<br>
|
||||||
|
<br>
|
||||||
|
<strong>Usage:</strong><br>
|
||||||
|
<code>!exploitdb <search_term> [max_results]</code><br>
|
||||||
|
<br>
|
||||||
|
<strong>Parameters:</strong><br>
|
||||||
|
• <strong>search_term</strong> (required) - Software name, CVE number, or vulnerability type<br>
|
||||||
|
• <strong>max_results</strong> (optional) - Number of results to return (1-10, default: 5)<br>
|
||||||
|
<br>
|
||||||
|
<strong>Examples:</strong><br>
|
||||||
|
<code>!exploitdb wordpress</code> - Search for WordPress exploits<br>
|
||||||
|
<code>!exploitdb apache 3</code> - Get 3 Apache exploits<br>
|
||||||
|
<code>!exploitdb windows privilege escalation</code> - Search for Windows privesc<br>
|
||||||
|
<code>!exploitdb CVE-2021-44228</code> - Search by CVE number<br>
|
||||||
|
<code>!exploitdb linux kernel 10</code> - Get 10 Linux kernel exploits<br>
|
||||||
|
<code>!exploitdb sql injection</code> - Search for SQL injection exploits<br>
|
||||||
|
<br>
|
||||||
|
<strong>Output Includes:</strong><br>
|
||||||
|
• Exploit title and description<br>
|
||||||
|
• EDB-ID (Exploit Database ID)<br>
|
||||||
|
• Exploit type (webapps, local, remote, etc.)<br>
|
||||||
|
• Platform/OS (PHP, Linux, Windows, etc.)<br>
|
||||||
|
• Author name<br>
|
||||||
|
• Publication date<br>
|
||||||
|
• Direct link to full exploit code<br>
|
||||||
|
<br>
|
||||||
|
<strong>Notes:</strong><br>
|
||||||
|
• Searches the official Exploit-DB CSV database<br>
|
||||||
|
• May take a few seconds on first use (downloads database)<br>
|
||||||
|
• Falls back to search links if database unavailable<br>
|
||||||
|
<br>
|
||||||
|
<em>⚠️ Use responsibly and only on systems you have permission to test.</em>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🛡️ <strong>!headers <url></strong></summary>
|
||||||
|
<p>Comprehensive HTTP security header analysis with security scoring and recommendations.</p>
|
||||||
|
<p><strong>Features:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Security scoring (0-100) with color-coded ratings</li>
|
||||||
|
<li>Critical security header validation and configuration checking</li>
|
||||||
|
<li>HTTP to HTTPS redirect chain analysis</li>
|
||||||
|
<li>SSL certificate information for HTTPS sites</li>
|
||||||
|
<li>Information disclosure header detection</li>
|
||||||
|
<li>Actionable security recommendations</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Security Headers Analyzed:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Strict-Transport-Security</code> - HSTS enforcement</li>
|
||||||
|
<li><code>Content-Security-Policy</code> - XSS protection</li>
|
||||||
|
<li><code>X-Frame-Options</code> - Clickjacking protection</li>
|
||||||
|
<li><code>X-Content-Type-Options</code> - MIME sniffing prevention</li>
|
||||||
|
<li><code>Referrer-Policy</code> - Referrer control</li>
|
||||||
|
<li><code>Feature-Policy</code> - Browser feature restrictions</li>
|
||||||
|
<li>Server information headers</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Security Ratings:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>🟢 <strong>Excellent (80-100)</strong> - Strong configuration</li>
|
||||||
|
<li>🟡 <strong>Good (60-79)</strong> - Moderate, needs improvement</li>
|
||||||
|
<li>🟠 <strong>Fair (40-59)</strong> - Basic, significant improvements needed</li>
|
||||||
|
<li>🔴 <strong>Poor (0-39)</strong> - Weak configuration</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Examples:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!headers example.com</code></li>
|
||||||
|
<li><code>!headers https://github.com</code></li>
|
||||||
|
<li><code>!headers localhost:3000</code></li>
|
||||||
|
<li><code>!headers subdomain.target.com</code></li>
|
||||||
|
</ul>
|
||||||
|
<p><em>Provides enterprise-grade security analysis for penetration testers and developers</em></p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🔄 <strong>!hashid <hash></strong></summary>
|
||||||
|
<p>Advanced hash type identification with confidence scoring and tool recommendations.</p>
|
||||||
|
<p><strong>Features:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>100+ hash types including modern, legacy, and exotic algorithms</li>
|
||||||
|
<li>Color-coded confidence scoring (🟢 Very High to 🔴 Low)</li>
|
||||||
|
<li>Hashcat mode numbers and John the Ripper format names</li>
|
||||||
|
<li>Context-aware parsing for various hash formats</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Supported Categories:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Modern:</strong> yescrypt, scrypt, Argon2, bcrypt</li>
|
||||||
|
<li><strong>Unix/Linux:</strong> SHA-512/256 Crypt, MD5 Crypt, apr1</li>
|
||||||
|
<li><strong>Raw Hashes:</strong> MD5, SHA family, SHA-3, NTLM, LM</li>
|
||||||
|
<li><strong>Databases:</strong> MySQL, PostgreSQL, Oracle, MSSQL</li>
|
||||||
|
<li><strong>Web/CMS:</strong> WordPress, Drupal, phpBB3, Django</li>
|
||||||
|
<li><strong>LDAP:</strong> SSHA, SMD5, LDAP crypt formats</li>
|
||||||
|
<li><strong>Network:</strong> NetNTLMv1/v2, Kerberos</li>
|
||||||
|
<li><strong>Exotic:</strong> Whirlpool, RIPEMD, GOST, BLAKE2</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Tool Integration:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Hashcat:</strong> Mode numbers for <code>-m</code> parameter</li>
|
||||||
|
<li><strong>John:</strong> Format names for <code>--format=</code> parameter</li>
|
||||||
|
<li>Multi-tool compatibility</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Examples:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!hashid 5d41402abc4b2a76b9719d911017c592</code> (MD5)</li>
|
||||||
|
<li><code>!hashid aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d</code> (SHA-1)</li>
|
||||||
|
<li><code>!hashid $6$rounds=5000$salt$hash...</code> (SHA-512 Crypt)</li>
|
||||||
|
<li><code>!hashid $y$j9T$...</code> (yescrypt - modern Linux)</li>
|
||||||
|
<li><code>!hashid 8846f7eaee8fb117ad06bdd830b7586c</code> (NTLM)</li>
|
||||||
|
<li><code>!hashid *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19</code> (MySQL)</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Confidence Legend:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>🟢 Very High (90-100%) - Single definitive match</li>
|
||||||
|
<li>🟡 High (80-89%) - Strong match with minor alternatives</li>
|
||||||
|
<li>🟠 Medium (60-79%) - Multiple plausible matches</li>
|
||||||
|
<li>🔴 Low (0-59%) - Uncertain, needs context</li>
|
||||||
|
</ul>
|
||||||
|
<p><em>Essential for penetration testers, forensic analysts, and password cracking</em></p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🔐 <strong>!sslscan <domain[:port]></strong></summary>
|
||||||
|
<p>Comprehensive SSL/TLS security scanning and analysis with vulnerability detection.</p>
|
||||||
|
<p><strong>Features:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>TLS 1.0-1.3 protocol support testing with security scoring</li>
|
||||||
|
<li>Certificate chain validation, expiration, and signature analysis</li>
|
||||||
|
<li>25+ cipher suite testing with strength classification</li>
|
||||||
|
<li>Vulnerability detection (POODLE, weak ciphers, protocol issues)</li>
|
||||||
|
<li>0-100 security rating with color-coded assessment</li>
|
||||||
|
<li>PCI DSS and modern security standards compliance checking</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Security Checks:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Protocol Security:</strong> TLS 1.2/1.3 enforcement, insecure protocol detection</li>
|
||||||
|
<li><strong>Certificate Health:</strong> Expiration monitoring, signature validation</li>
|
||||||
|
<li><strong>Cipher Security:</strong> RC4, DES, 3DES, NULL cipher detection</li>
|
||||||
|
<li><strong>Modern Standards:</strong> Forward Secrecy, strong encryption</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Security Ratings:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>🟢 <strong>Excellent (90-100)</strong> - Modern TLS with strong security</li>
|
||||||
|
<li>🟡 <strong>Good (80-89)</strong> - Good security, minor improvements needed</li>
|
||||||
|
<li>🟠 <strong>Fair (60-79)</strong> - Moderate security, significant improvements</li>
|
||||||
|
<li>🔴 <strong>Poor (0-59)</strong> - Critical issues requiring immediate attention</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Examples:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><code>!sslscan example.com</code></li>
|
||||||
|
<li><code>!sslscan github.com:443</code></li>
|
||||||
|
<li><code>!sslscan localhost:8443</code></li>
|
||||||
|
<li><code>!sslscan 192.168.1.1:443</code></li>
|
||||||
|
</ul>
|
||||||
|
<p><em>Essential for security teams, system administrators, and developers ensuring TLS compliance</em><br>
|
||||||
|
<em>Note: SSLv2/SSLv3 testing limited by Python security features</em></p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<details><summary>📸 <strong>!sd [prompt]</strong></summary>
|
||||||
|
<p>Generates images using self-hosted Stable Diffusion. Supports options: --steps, --cfg, --h, --w, --neg, --sampler. Uses queuing system to handle multiple requests. See available options using just '!sd'.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>📄 <strong>!text [prompt]</strong></summary>
|
||||||
|
<p>Generates text using Ollama's Mistral 7B Instruct model. Options: --max_tokens, --temperature. Uses queuing system for sequential processing.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>📰 <strong>!xkcd</strong></summary>
|
||||||
|
<p>Fetches and displays a random XKCD comic. Downloads comic image and sends it directly to the chat room.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🎬 <strong>YouTube Features</strong></summary>
|
||||||
|
<p>Automatic preview when YouTube links are posted. Shows video info, description, and attempts to fetch lyrics. Also supports !yt [search terms] for direct YouTube searching.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>⏱️ <strong>!cron [add|remove] [room_id] [cron_entry] [command]</strong></summary>
|
||||||
|
<p>Schedule automated commands using cron syntax. Add or remove cron jobs for specific rooms and commands.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🔧 <strong>Admin Commands</strong></summary>
|
||||||
|
<p>
|
||||||
|
<strong>!set [option] [value]</strong> - Set configuration options (admin_user, prefix)<br>
|
||||||
|
<strong>!get [option]</strong> - Get configuration values<br>
|
||||||
|
<strong>!saveconf</strong> - Save current configuration<br>
|
||||||
|
<strong>!loadconf</strong> - Load saved configuration<br>
|
||||||
|
<strong>!show</strong> - Display current configuration<br>
|
||||||
|
<strong>!reset</strong> - Reset configuration to defaults<br>
|
||||||
|
<strong>!load [plugin]</strong> - Load a plugin<br>
|
||||||
|
<strong>!unload [plugin]</strong> - Unload a plugin<br>
|
||||||
|
<strong>!reload</strong> - Reload all plugins<br>
|
||||||
|
<strong>!disable [plugin] [room_id]</strong> - Disable a plugin for specific room<br>
|
||||||
|
<strong>!enable [plugin] [room_id]</strong> - Enable a plugin for specific room<br>
|
||||||
|
<strong>!rehash</strong> - Reload configuration<br>
|
||||||
|
<em>Note: Admin commands require admin_user privileges</em>
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary><strong>🤖 Funguy Bot AI Commands</strong></summary>
|
||||||
|
<p>
|
||||||
|
<strong>Creative & Writing</strong>: !write, !script, !author, !poem, !rap, !story, !comic, !motiv, !debate, !crit, !litcrit<br>
|
||||||
|
<strong>Technical</strong>: !tech, !dev, !py, !php, !regex, !math, !web, !it, !security, !ai, !ml, !data, !game, !gaming<br>
|
||||||
|
<strong>Professional</strong>: !seo, !recruit, !coach, !devrel, !sales, !ceo, !mgmt, !startup, !invest, !fin, !acad<br>
|
||||||
|
<strong>Educational</strong>: !tutor, !teach, !edu, !hist, !astro, !chem, !psych, !meditate, !socrat, !philos<br>
|
||||||
|
<strong>Lifestyle</strong>: !fit, !health, !diet, !cook, !travel, !art, !music, !film, !selfhelp<br>
|
||||||
|
<strong>Specialized</strong>: !legal, !medical, !realest, !auto, !fashion, !design, !interior, !florist<br>
|
||||||
|
<strong>Communication</strong>: !pron, !spk, !speak, !eloc, !comm, !msg, !langdet<br>
|
||||||
|
<strong>Business</strong>: !eth, !browse, !search, !create, !review, !curation, !domain<br>
|
||||||
|
<strong>Entertainment</strong>: !char, !adv, !advgame, !esc, !title, !stats, !prompt<br>
|
||||||
|
<strong>Technical Specialties</strong>: !intv, !plag, !trv, !foot, !rel, !etymo, !magic, !counsel, !behavior, !mh, !log, !dental, !acc, !chef, !tea, !telemed, !law, !trans, !chess, !time, !dream, !r, !emergency, !worksheet, !test, !create, !guide, !diag, !therapy, !gen, !drunk, !rec, !techtrans, !proof, !spirit, !friend, !chat, !wiki, !kanji, !note, !enhance, !nav, !hypno, !critic, !comp, !journo, !pscoach, !makeup, !childcare, !writing, !syn, !shop, !dining<br>
|
||||||
|
|
||||||
|
<em>Each AI command uses specialized prompts optimized for different domains and interfaces with local AI models. Consult ai.json</em>
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>🌟 <strong>Funguy Bot Credits</strong></summary>
|
||||||
|
<p>
|
||||||
|
<strong>🧙♂️ Creator & Developer</strong>: HB is the author of 🍄Funguy Bot🍄. (@hashborgir:mozilla.org)<br>
|
||||||
|
<strong>🚀 Development Context</strong>: Created during recovery from two-level cervical spinal surgery (CDA Cervical Discectomy and Disc Arthroplasty)<br>
|
||||||
|
<br>
|
||||||
|
<strong>Join our Matrix Room</strong>: <a href="https://matrix.to/#/#selfhosting:mozilla.org">Self-hosting | Security | Sysadmin | Homelab | Programming</a>
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await bot.api.send_markdown_message(room.room_id, commands_message)
|
await bot.api.send_markdown_message(room.room_id, commands_message)
|
||||||
logging.info("Sent help documentation to the room")
|
logging.info("Sent help documentation to the room")
|
||||||
|
@@ -50,38 +50,53 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
Args:
|
Args:
|
||||||
room (Room): The Matrix room where the command was invoked.
|
room (Room): The Matrix room where the command was invoked.
|
||||||
message (RoomMessage): The message object containing the command.
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot instance.
|
||||||
|
prefix (str): The bot command prefix.
|
||||||
|
config (FunguyConfig): The bot configuration instance.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
|
# Check if the message matches the command pattern and is not from this bot
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("isup"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("isup"):
|
||||||
|
# Log that the !isup command has been received
|
||||||
logging.info("Received !isup command")
|
logging.info("Received !isup command")
|
||||||
args = match.args()
|
args = match.args()
|
||||||
|
# Check if the command has exactly one argument
|
||||||
if len(args) != 1:
|
if len(args) != 1:
|
||||||
|
# If the command does not have exactly one argument, send usage message
|
||||||
await bot.api.send_markdown_message(room.room_id, "Usage: !isup <ipv4/ipv6/domain>")
|
await bot.api.send_markdown_message(room.room_id, "Usage: !isup <ipv4/ipv6/domain>")
|
||||||
logging.info("Sent usage message to the room")
|
logging.info("Sent usage message to the room")
|
||||||
return
|
return
|
||||||
|
|
||||||
target = args[0]
|
target = args[0]
|
||||||
|
|
||||||
# DNS resolution
|
# Perform DNS resolution
|
||||||
try:
|
try:
|
||||||
ip_address = socket.gethostbyname(target)
|
ip_address = socket.gethostbyname(target)
|
||||||
|
# Log successful DNS resolution
|
||||||
logging.info(f"DNS resolution successful for {target}: {ip_address}")
|
logging.info(f"DNS resolution successful for {target}: {ip_address}")
|
||||||
|
# Send DNS resolution success message
|
||||||
await bot.api.send_markdown_message(room.room_id, f"✅ DNS resolution successful for **{target}**: **{ip_address}** (A record)")
|
await bot.api.send_markdown_message(room.room_id, f"✅ DNS resolution successful for **{target}**: **{ip_address}** (A record)")
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
|
# Log DNS resolution failure
|
||||||
logging.info(f"DNS resolution failed for {target}")
|
logging.info(f"DNS resolution failed for {target}")
|
||||||
|
# Send DNS resolution failure message
|
||||||
await bot.api.send_markdown_message(room.room_id, f"❌ DNS resolution failed for **{target}**")
|
await bot.api.send_markdown_message(room.room_id, f"❌ DNS resolution failed for **{target}**")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check HTTP/HTTPS services
|
# Check HTTP/HTTPS services
|
||||||
if await check_http(target):
|
if await check_http(target):
|
||||||
|
# If HTTP service is up, send HTTP service up message
|
||||||
await bot.api.send_markdown_message(room.room_id, f"🖧 **{target}** HTTP service is up")
|
await bot.api.send_markdown_message(room.room_id, f"🖧 **{target}** HTTP service is up")
|
||||||
logging.info(f"{target} HTTP service is up")
|
logging.info(f"{target} HTTP service is up")
|
||||||
elif await check_https(target):
|
elif await check_https(target):
|
||||||
|
# If HTTPS service is up, send HTTPS service up message
|
||||||
await bot.api.send_markdown_message(room.room_id, f"🖧 **{target}** HTTPS service is up")
|
await bot.api.send_markdown_message(room.room_id, f"🖧 **{target}** HTTPS service is up")
|
||||||
logging.info(f"{target} HTTPS service is up")
|
logging.info(f"{target} HTTPS service is up")
|
||||||
else:
|
else:
|
||||||
|
# If both HTTP and HTTPS services are down, send service down message
|
||||||
await bot.api.send_markdown_message(room.room_id, f"😕 **{target}** HTTP/HTTPS services are down")
|
await bot.api.send_markdown_message(room.room_id, f"😕 **{target}** HTTP/HTTPS services are down")
|
||||||
logging.info(f"{target} HTTP/HTTPS services are down")
|
logging.info(f"{target} HTTP/HTTPS services are down")
|
||||||
|
|
||||||
|
@@ -1,41 +1,166 @@
|
|||||||
"""
|
"""
|
||||||
This plugin provides a command for the admin to load a plugin
|
Plugin for providing a command for the admin to load a plugin.
|
||||||
"""
|
"""
|
||||||
# plugins/load_plugin.py
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import importlib
|
import importlib
|
||||||
import simplematrixbotlib as botlib
|
import simplematrixbotlib as botlib
|
||||||
|
import sys # Import sys module for unloading plugins
|
||||||
|
|
||||||
|
# Dictionary to store loaded plugins
|
||||||
PLUGINS = {}
|
PLUGINS = {}
|
||||||
|
|
||||||
|
# Whitelist of allowed plugins to prevent arbitrary code execution
|
||||||
|
ALLOWED_PLUGINS = {'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma',
|
||||||
|
'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion',
|
||||||
|
'xkcd', 'youtube-preview', 'youtube-search'}
|
||||||
|
|
||||||
async def load_plugin(plugin_name):
|
async def load_plugin(plugin_name):
|
||||||
|
"""
|
||||||
|
Asynchronously loads a plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_name (str): The name of the plugin to load.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the plugin is loaded successfully, False otherwise.
|
||||||
|
"""
|
||||||
|
# Validate plugin name against whitelist
|
||||||
|
if plugin_name not in ALLOWED_PLUGINS:
|
||||||
|
logging.error(f"Plugin '{plugin_name}' is not whitelisted")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Verify that the plugin file exists in the plugins directory
|
||||||
|
plugin_path = os.path.join("plugins", f"{plugin_name}.py")
|
||||||
|
if not os.path.isfile(plugin_path):
|
||||||
|
logging.error(f"Plugin file not found: {plugin_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module(f"plugins.{plugin_name}")
|
# Create a mapping of whitelisted plugins to their module paths
|
||||||
|
plugin_modules = {
|
||||||
|
'ai': 'plugins.ai',
|
||||||
|
'config': 'plugins.config',
|
||||||
|
'cron': 'plugins.cron',
|
||||||
|
'date': 'plugins.date',
|
||||||
|
'fortune': 'plugins.fortune',
|
||||||
|
'help': 'plugins.help',
|
||||||
|
'isup': 'plugins.isup',
|
||||||
|
'karma': 'plugins.karma',
|
||||||
|
'loadplugin': 'plugins.loadplugin',
|
||||||
|
'plugins': 'plugins.plugins',
|
||||||
|
'proxy': 'plugins.proxy',
|
||||||
|
'sd_text': 'plugins.sd_text',
|
||||||
|
'stable-diffusion': 'plugins.stable-diffusion',
|
||||||
|
'xkcd': 'plugins.xkcd',
|
||||||
|
'youtube-preview': 'plugins.youtube-preview',
|
||||||
|
'youtube-search': 'plugins.youtube-search',
|
||||||
|
'weather': 'plugins.weather',
|
||||||
|
'urbandictionary': 'plugins.urbandictionary',
|
||||||
|
'bitcoin':'plugins.bitcoin',
|
||||||
|
'dns':'plugins.dns',
|
||||||
|
'shodan':'plugins.shodan',
|
||||||
|
'dnsdumpster': 'plugins.dnsdumpster',
|
||||||
|
'exploitdb': 'plugins.exploitdb',
|
||||||
|
'headers': 'plugins.headers',
|
||||||
|
'hashid': 'plugins.hashid',
|
||||||
|
'sslscan': 'plugins.sslscan'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the module path from the mapping
|
||||||
|
module_path = plugin_modules.get(plugin_name)
|
||||||
|
if not module_path:
|
||||||
|
logging.error(f"Plugin '{plugin_name}' not found in plugin mapping")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Import the plugin module using the validated module path
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
# Add the plugin module to the PLUGINS dictionary
|
||||||
PLUGINS[plugin_name] = module
|
PLUGINS[plugin_name] = module
|
||||||
logging.info(f"Loaded plugin: {plugin_name}")
|
logging.info(f"Loaded plugin: {plugin_name}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Log an error if the plugin fails to load
|
||||||
logging.error(f"Error loading plugin {plugin_name}: {e}")
|
logging.error(f"Error loading plugin {plugin_name}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def handle_command(room, message, bot, prefix, config):
|
async def unload_plugin(plugin_name):
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
"""
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("load"):
|
Asynchronously unloads a plugin.
|
||||||
if str(message.sender) == config.admin_user:
|
|
||||||
args = match.args()
|
Args:
|
||||||
if len(args) != 1:
|
plugin_name (str): The name of the plugin to unload.
|
||||||
await bot.api.send_text_message(room.room_id, "Usage: !load <plugin>")
|
|
||||||
else:
|
Returns:
|
||||||
plugin_name = args[0]
|
bool: True if the plugin is unloaded successfully, False otherwise.
|
||||||
if plugin_name not in PLUGINS:
|
"""
|
||||||
success = await load_plugin(plugin_name)
|
try:
|
||||||
if success:
|
if plugin_name in PLUGINS:
|
||||||
await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' loaded successfully")
|
del PLUGINS[plugin_name] # Remove the plugin from the PLUGINS dictionary
|
||||||
else:
|
del sys.modules[f"plugins.{plugin_name}"] # Unload the plugin module from sys.modules
|
||||||
await bot.api.send_text_message(room.room_id, f"Error loading plugin '{plugin_name}'")
|
logging.info(f"Unloaded plugin: {plugin_name}")
|
||||||
else:
|
return True
|
||||||
await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' is already loaded")
|
|
||||||
else:
|
else:
|
||||||
await bot.api.send_text_message(room.room_id, "You are not authorized to load plugins.")
|
logging.warning(f"Plugin '{plugin_name}' is not loaded")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
# Log an error if the plugin fails to unload
|
||||||
|
logging.error(f"Error unloading plugin {plugin_name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Asynchronously handles the command to load or unload a plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (MatrixBot): The Matrix bot instance.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): The bot's configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix():
|
||||||
|
command = match.command()
|
||||||
|
if command == "load":
|
||||||
|
if str(message.sender) == config.admin_user:
|
||||||
|
args = match.args()
|
||||||
|
if len(args) != 1:
|
||||||
|
# Send usage message if the command format is incorrect
|
||||||
|
await bot.api.send_text_message(room.room_id, "Usage: !load <plugin>")
|
||||||
|
else:
|
||||||
|
plugin_name = args[0]
|
||||||
|
# Check if the plugin is not already loaded
|
||||||
|
if plugin_name not in PLUGINS:
|
||||||
|
# Load the plugin
|
||||||
|
success = await load_plugin(plugin_name)
|
||||||
|
if success:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' loaded successfully")
|
||||||
|
else:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error loading plugin '{plugin_name}'")
|
||||||
|
else:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' is already loaded")
|
||||||
|
else:
|
||||||
|
# Send unauthorized message if the sender is not the admin
|
||||||
|
await bot.api.send_text_message(room.room_id, "You are not authorized to load plugins.")
|
||||||
|
elif command == "unload":
|
||||||
|
if str(message.sender) == config.admin_user:
|
||||||
|
args = match.args()
|
||||||
|
if len(args) != 1:
|
||||||
|
# Send usage message if the command format is incorrect
|
||||||
|
await bot.api.send_text_message(room.room_id, "Usage: !unload <plugin>")
|
||||||
|
else:
|
||||||
|
plugin_name = args[0]
|
||||||
|
# Unload the plugin
|
||||||
|
success = await unload_plugin(plugin_name)
|
||||||
|
if success:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' unloaded successfully")
|
||||||
|
else:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error unloading plugin '{plugin_name}'")
|
||||||
|
else:
|
||||||
|
# Send unauthorized message if the sender is not the admin
|
||||||
|
await bot.api.send_text_message(room.room_id, "You are not authorized to unload plugins.")
|
||||||
|
171
plugins/proxy.py
171
plugins/proxy.py
@@ -9,44 +9,50 @@ import logging
|
|||||||
import random
|
import random
|
||||||
import requests
|
import requests
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import concurrent.futures
|
||||||
import simplematrixbotlib as botlib
|
import simplematrixbotlib as botlib
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
SOCKS5_LIST_URL = 'https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/socks5.txt'
|
SOCKS5_LIST_URL = 'https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/socks5.txt'
|
||||||
MAX_TRIES = 5
|
# SOCKS5_LIST_URL = 'https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/protocols/socks5/data.txt'
|
||||||
|
MAX_TRIES = 64
|
||||||
PROXY_LIST_FILENAME = 'socks5.txt'
|
PROXY_LIST_FILENAME = 'socks5.txt'
|
||||||
PROXY_LIST_EXPIRATION = timedelta(hours=8)
|
PROXY_LIST_EXPIRATION = timedelta(hours=8)
|
||||||
|
MAX_THREADS = 128
|
||||||
|
PROXIES_DB_FILE = 'proxies.db'
|
||||||
|
MAX_PROXIES_IN_DB = 10
|
||||||
|
|
||||||
async def test_socks5_proxy(proxy):
|
# Setup verbose logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
def test_proxy(proxy):
|
||||||
"""
|
"""
|
||||||
Test a SOCKS5 proxy by attempting a connection and sending a request through it.
|
Test a SOCKS5 proxy and return the outcome.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
proxy (str): The SOCKS5 proxy address in the format 'ip:port'.
|
proxy (str): The SOCKS5 proxy address in the format 'ip:port'.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the proxy is working, False otherwise.
|
tuple: (bool: success, str: proxy, int: latency)
|
||||||
float: The latency in milliseconds if the proxy is working, None otherwise.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
ip, port = proxy.split(':')
|
ip, port = proxy.split(':')
|
||||||
|
logging.info(f"Testing SOCKS5 proxy: {ip}:{port}")
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
with socket.create_connection((ip, int(port)), timeout=5) as client:
|
with socket.create_connection((ip, int(port)), timeout=12) as client:
|
||||||
client.sendall(b'\x05\x01\x00')
|
client.sendall(b'\x05\x01\x00')
|
||||||
response = client.recv(2)
|
response = client.recv(2)
|
||||||
if response == b'\x05\x00':
|
if response == b'\x05\x00':
|
||||||
latency = int(round((time.time() - start_time) * 1000, 0))
|
latency = int(round((time.time() - start_time) * 1000, 0))
|
||||||
logging.info(f"Successful connection to SOCKS5 proxy {proxy}. Latency: {latency} ms")
|
return True, proxy, latency
|
||||||
return True, latency
|
|
||||||
else:
|
else:
|
||||||
logging.info(f"Failed to connect to SOCKS5 proxy {proxy}: Connection refused")
|
return False, proxy, None
|
||||||
return False, None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error testing SOCKS5 proxy {proxy}: {e}")
|
return False, proxy, None
|
||||||
return False, None
|
|
||||||
|
|
||||||
async def download_proxy_list():
|
async def download_proxy_list():
|
||||||
"""
|
"""
|
||||||
@@ -72,6 +78,69 @@ async def download_proxy_list():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_db_for_proxy():
|
||||||
|
"""
|
||||||
|
Check the proxies database for a working proxy.
|
||||||
|
If found, test the proxy and remove it from the database if it doesn't work.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str or None: The working proxy if found, None otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(PROXIES_DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS proxies (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
proxy TEXT,
|
||||||
|
latency INTEGER,
|
||||||
|
status TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
cursor.execute("SELECT proxy, latency FROM proxies WHERE status = 'working' AND latency < 3000 ORDER BY RANDOM() LIMIT 1")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if result:
|
||||||
|
proxy, latency = result
|
||||||
|
success, _, _ = test_proxy(proxy)
|
||||||
|
if success:
|
||||||
|
return proxy, latency
|
||||||
|
else:
|
||||||
|
cursor.execute("DELETE FROM proxies WHERE proxy = ?", (proxy,))
|
||||||
|
conn.commit()
|
||||||
|
logging.info(f"Removed non-working proxy from the database: {proxy}")
|
||||||
|
return None, None
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error checking proxies database: {e}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def save_proxy_to_db(proxy, latency):
|
||||||
|
"""
|
||||||
|
Save a working proxy to the proxies database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
proxy (str): The working proxy to be saved.
|
||||||
|
latency (int): Latency of the proxy.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(PROXIES_DB_FILE) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS proxies (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
proxy TEXT,
|
||||||
|
latency INTEGER,
|
||||||
|
status TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
cursor.execute("INSERT INTO proxies (proxy, latency, status) VALUES (?, ?, 'working')", (proxy, latency))
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error saving proxy to database: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def handle_command(room, message, bot, prefix, config):
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
"""
|
"""
|
||||||
Function to handle the !proxy command.
|
Function to handle the !proxy command.
|
||||||
@@ -87,34 +156,58 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("proxy"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("proxy"):
|
||||||
logging.info("Received !proxy command")
|
logging.info("Received !proxy command")
|
||||||
|
|
||||||
# Download proxy list if needed
|
# Check database for a working proxy
|
||||||
if not await download_proxy_list():
|
working_proxy, latency = check_db_for_proxy()
|
||||||
await bot.api.send_markdown_message(room.room_id, "Error downloading proxy list")
|
if working_proxy:
|
||||||
logging.error("Error downloading proxy list")
|
await bot.api.send_markdown_message(room.room_id,
|
||||||
|
f"✅ Using cached working SOCKS5 Proxy: **{working_proxy}** - Latency: **{latency} ms**")
|
||||||
|
logging.info(f"Using cached working SOCKS5 proxy {working_proxy}")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
# Download proxy list if needed
|
||||||
# Read proxies from file
|
else:
|
||||||
with open(PROXY_LIST_FILENAME, 'r') as f:
|
if not await download_proxy_list():
|
||||||
socks5_proxies = f.read().splitlines()
|
await bot.api.send_markdown_message(room.room_id, "Error downloading proxy list")
|
||||||
random.shuffle(socks5_proxies)
|
logging.error("Error downloading proxy list")
|
||||||
|
return
|
||||||
|
|
||||||
# Test proxies
|
try:
|
||||||
socks5_proxy = None
|
# Read proxies from file
|
||||||
for proxy in socks5_proxies[:MAX_TRIES]:
|
with open(PROXY_LIST_FILENAME, 'r') as f:
|
||||||
success, latency = await test_socks5_proxy(proxy)
|
socks5_proxies = [line.replace("socks5://", "") for line in f.read().splitlines()]
|
||||||
if success:
|
random.shuffle(socks5_proxies)
|
||||||
socks5_proxy = proxy
|
|
||||||
break
|
# Test proxies concurrently
|
||||||
|
tested_proxies = 0
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
|
||||||
|
futures = []
|
||||||
|
for proxy in socks5_proxies[:MAX_TRIES]:
|
||||||
|
futures.append(executor.submit(test_proxy, proxy))
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
success, proxy, latency = future.result()
|
||||||
|
if success:
|
||||||
|
await bot.api.send_markdown_message(room.room_id,
|
||||||
|
f"✅ Anonymous SOCKS5 Proxy: **{proxy}** - Latency: **{latency} ms**")
|
||||||
|
logging.info(f"Sent SOCKS5 proxy {proxy} to the room")
|
||||||
|
save_proxy_to_db(proxy, latency) # Save working proxy to the database
|
||||||
|
tested_proxies += 1
|
||||||
|
if tested_proxies >= MAX_PROXIES_IN_DB:
|
||||||
|
break # Stop testing proxies once MAX_PROXIES_IN_DB are saved to the database
|
||||||
|
|
||||||
|
# Check database for a working proxy after testing
|
||||||
|
working_proxy, latency = check_db_for_proxy()
|
||||||
|
if working_proxy:
|
||||||
|
await bot.api.send_markdown_message(room.room_id,
|
||||||
|
f"✅ Using cached working SOCKS5 Proxy: **{working_proxy}** - Latency: **{latency} ms**")
|
||||||
|
logging.info(f"Using cached working SOCKS5 proxy {working_proxy}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# If no working proxy found after testing
|
||||||
|
await bot.api.send_markdown_message(room.room_id, "❌ No working anonymous SOCKS5 proxy found")
|
||||||
|
logging.info("No working anonymous SOCKS5 proxy found")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error handling !proxy command: {e}")
|
||||||
|
await bot.api.send_markdown_message(room.room_id, "❌ Error handling !proxy command")
|
||||||
|
|
||||||
# Send the first working anonymous proxy of each type to the Matrix room
|
|
||||||
if socks5_proxy:
|
|
||||||
await bot.api.send_markdown_message(room.room_id, f"✅ Anonymous SOCKS5 Proxy: **{socks5_proxy}** - Latency: **{latency} ms**")
|
|
||||||
logging.info(f"Sent SOCKS5 proxy {socks5_proxy} to the room")
|
|
||||||
else:
|
|
||||||
await bot.api.send_markdown_message(room.room_id, "❌ No working anonymous SOCKS5 proxy found")
|
|
||||||
logging.info("No working anonymous SOCKS5 proxy found")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error handling !proxy command: {e}")
|
|
||||||
await bot.api.send_markdown_message(room.room_id, "❌ Error handling !proxy command")
|
|
||||||
|
95
plugins/sd_text.py
Normal file
95
plugins/sd_text.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""
|
||||||
|
Plugin for generating text using Ollama's Mistral 7B Instruct model and sending it to a Matrix chat room.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from asyncio import Queue
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# Queue to store pending commands
|
||||||
|
command_queue = Queue()
|
||||||
|
|
||||||
|
API_URL = "http://localhost:11434/api/generate"
|
||||||
|
MODEL_NAME = "mistral:7b-instruct"
|
||||||
|
|
||||||
|
async def process_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Queue and process !text commands sequentially.
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.prefix() and match.command("text"):
|
||||||
|
if command_queue.empty():
|
||||||
|
await handle_command(room, message, bot, prefix, config)
|
||||||
|
else:
|
||||||
|
await command_queue.put((room, message, bot, prefix, config))
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Send the prompt to Ollama API and return the generated text.
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if not (match.prefix() and match.command("text")):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse optional arguments
|
||||||
|
parser = argparse.ArgumentParser(description='Generate text using Ollama API')
|
||||||
|
parser.add_argument('--max_tokens', type=int, default=512, help='Maximum tokens to generate')
|
||||||
|
parser.add_argument('--temperature', type=float, default=0.7, help='Temperature for generation')
|
||||||
|
parser.add_argument('prompt', nargs='+', help='Prompt for the model')
|
||||||
|
|
||||||
|
try:
|
||||||
|
args = parser.parse_args(message.body.split()[1:]) # Skip command itself
|
||||||
|
prompt = ' '.join(args.prompt).strip()
|
||||||
|
|
||||||
|
if not prompt:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Usage: !text <your prompt here>")
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": MODEL_NAME,
|
||||||
|
"prompt": prompt,
|
||||||
|
"max_tokens": args.max_tokens,
|
||||||
|
"temperature": args.temperature,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(API_URL, json=payload, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
r = response.json()
|
||||||
|
|
||||||
|
generated_text = r.get("response", "").strip()
|
||||||
|
if not generated_text:
|
||||||
|
generated_text = "(No response from model)"
|
||||||
|
|
||||||
|
await bot.api.send_text_message(room.room_id, generated_text)
|
||||||
|
|
||||||
|
except argparse.ArgumentError as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Argument error: {e}")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error connecting to Ollama API: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Unexpected error: {e}")
|
||||||
|
finally:
|
||||||
|
# Process next command from the queue, if any
|
||||||
|
if not command_queue.empty():
|
||||||
|
next_command = await command_queue.get()
|
||||||
|
await handle_command(*next_command)
|
||||||
|
|
||||||
|
def print_help():
|
||||||
|
"""
|
||||||
|
Generates help text for the !text command.
|
||||||
|
"""
|
||||||
|
return """
|
||||||
|
<p>Generate text using Ollama's Mistral 7B Instruct model</p>
|
||||||
|
|
||||||
|
<p>Usage:</p>
|
||||||
|
<ul>
|
||||||
|
<li>!text <prompt> - Basic prompt for the model</li>
|
||||||
|
<li>Optional arguments:</li>
|
||||||
|
<ul>
|
||||||
|
<li>--max_tokens MAX_TOKENS - Maximum tokens to generate (default 512)</li>
|
||||||
|
<li>--temperature TEMPERATURE - Sampling temperature (default 0.7)</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
"""
|
330
plugins/shodan.py
Normal file
330
plugins/shodan.py
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides Shodan.io integration for security research and reconnaissance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
parent_dir = os.path.dirname(plugin_dir)
|
||||||
|
dotenv_path = os.path.join(parent_dir, '.env')
|
||||||
|
load_dotenv(dotenv_path)
|
||||||
|
|
||||||
|
SHODAN_API_KEY = os.getenv("SHODAN_KEY", "")
|
||||||
|
SHODAN_API_BASE = "https://api.shodan.io"
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle Shodan commands.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("shodan"):
|
||||||
|
logging.info("Received !shodan command")
|
||||||
|
|
||||||
|
# Check if API key is configured
|
||||||
|
if not SHODAN_API_KEY:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Shodan API key not configured. Please set SHODAN_KEY environment variable."
|
||||||
|
)
|
||||||
|
logging.error("Shodan API key not configured")
|
||||||
|
return
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
if len(args) < 1:
|
||||||
|
await show_usage(room, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
subcommand = args[0].lower()
|
||||||
|
|
||||||
|
if subcommand == "ip":
|
||||||
|
if len(args) < 2:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Usage: !shodan ip <ip_address>")
|
||||||
|
return
|
||||||
|
ip = args[1]
|
||||||
|
await shodan_ip_lookup(room, bot, ip)
|
||||||
|
|
||||||
|
elif subcommand == "search":
|
||||||
|
if len(args) < 2:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Usage: !shodan search <query>")
|
||||||
|
return
|
||||||
|
query = ' '.join(args[1:])
|
||||||
|
await shodan_search(room, bot, query)
|
||||||
|
|
||||||
|
elif subcommand == "host":
|
||||||
|
if len(args) < 2:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Usage: !shodan host <domain/ip>")
|
||||||
|
return
|
||||||
|
host = args[1]
|
||||||
|
await shodan_host(room, bot, host)
|
||||||
|
|
||||||
|
elif subcommand == "count":
|
||||||
|
if len(args) < 2:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Usage: !shodan count <query>")
|
||||||
|
return
|
||||||
|
query = ' '.join(args[1:])
|
||||||
|
await shodan_count(room, bot, query)
|
||||||
|
|
||||||
|
else:
|
||||||
|
await show_usage(room, bot)
|
||||||
|
|
||||||
|
async def show_usage(room, bot):
|
||||||
|
"""Display Shodan command usage."""
|
||||||
|
usage = """
|
||||||
|
<strong>🔍 Shodan Commands:</strong>
|
||||||
|
|
||||||
|
<strong>!shodan ip <ip_address></strong> - Get detailed information about an IP
|
||||||
|
<strong>!shodan search <query></strong> - Search Shodan database
|
||||||
|
<strong>!shodan host <domain/ip></strong> - Get host information
|
||||||
|
<strong>!shodan count <query></strong> - Count results for a search query
|
||||||
|
|
||||||
|
<strong>Search Examples:</strong>
|
||||||
|
• <code>!shodan search apache</code>
|
||||||
|
• <code>!shodan search "port:22"</code>
|
||||||
|
• <code>!shodan search "country:US product:nginx"</code>
|
||||||
|
• <code>!shodan search "net:192.168.1.0/24"</code>
|
||||||
|
"""
|
||||||
|
await bot.api.send_markdown_message(room.room_id, usage)
|
||||||
|
|
||||||
|
async def shodan_ip_lookup(room, bot, ip):
|
||||||
|
"""Look up information about a specific IP address."""
|
||||||
|
try:
|
||||||
|
url = f"{SHODAN_API_BASE}/shodan/host/{ip}"
|
||||||
|
params = {"key": SHODAN_API_KEY}
|
||||||
|
|
||||||
|
logging.info(f"Fetching Shodan IP info for: {ip}")
|
||||||
|
response = requests.get(url, params=params, timeout=15)
|
||||||
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"No information found for IP: {ip}")
|
||||||
|
return
|
||||||
|
elif response.status_code == 401:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Invalid Shodan API key")
|
||||||
|
return
|
||||||
|
elif response.status_code != 200:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Shodan API error: {response.status_code}")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Format the response
|
||||||
|
output = f"<strong>🔍 Shodan IP Lookup: {ip}</strong><br><br>"
|
||||||
|
|
||||||
|
if data.get('country_name'):
|
||||||
|
output += f"<strong>📍 Location:</strong> {data.get('city', 'N/A')}, {data.get('country_name', 'N/A')}<br>"
|
||||||
|
|
||||||
|
if data.get('org'):
|
||||||
|
output += f"<strong>🏢 Organization:</strong> {data['org']}<br>"
|
||||||
|
|
||||||
|
if data.get('os'):
|
||||||
|
output += f"<strong>💻 Operating System:</strong> {data['os']}<br>"
|
||||||
|
|
||||||
|
if data.get('ports'):
|
||||||
|
output += f"<strong>🔌 Open Ports:</strong> {', '.join(map(str, data['ports']))}<br>"
|
||||||
|
|
||||||
|
output += f"<strong>🕒 Last Update:</strong> {data.get('last_update', 'N/A')}<br><br>"
|
||||||
|
|
||||||
|
# Show services
|
||||||
|
if data.get('data'):
|
||||||
|
output += "<strong>📡 Services:</strong><br>"
|
||||||
|
for service in data['data'][:5]: # Limit to first 5 services
|
||||||
|
port = service.get('port', 'N/A')
|
||||||
|
product = service.get('product', 'Unknown')
|
||||||
|
version = service.get('version', '')
|
||||||
|
banner = service.get('data', '')[:100] + "..." if len(service.get('data', '')) > 100 else service.get('data', '')
|
||||||
|
|
||||||
|
output += f" • <strong>Port {port}:</strong> {product} {version}<br>"
|
||||||
|
if banner:
|
||||||
|
output += f" <em>{banner}</em><br>"
|
||||||
|
|
||||||
|
if len(data['data']) > 5:
|
||||||
|
output += f" • ... and {len(data['data']) - 5} more services<br>"
|
||||||
|
|
||||||
|
# Wrap in collapsible if output is large
|
||||||
|
if len(output) > 500:
|
||||||
|
output = f"<details><summary><strong>🔍 Shodan IP Lookup: {ip}</strong></summary>{output}</details>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
logging.info(f"Sent Shodan IP info for {ip}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
|
||||||
|
logging.error("Shodan API timeout")
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error fetching Shodan data: {str(e)}")
|
||||||
|
logging.error(f"Error in shodan_ip_lookup: {e}")
|
||||||
|
|
||||||
|
async def shodan_search(room, bot, query):
|
||||||
|
"""Search the Shodan database."""
|
||||||
|
try:
|
||||||
|
url = f"{SHODAN_API_BASE}/shodan/host/search"
|
||||||
|
params = {
|
||||||
|
"key": SHODAN_API_KEY,
|
||||||
|
"query": query,
|
||||||
|
"minify": True,
|
||||||
|
"limit": 5 # Limit results to avoid huge responses
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f"Searching Shodan for: {query}")
|
||||||
|
response = requests.get(url, params=params, timeout=15)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
await handle_shodan_error(room, bot, response.status_code)
|
||||||
|
return
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if not data.get('matches'):
|
||||||
|
await bot.api.send_text_message(room.room_id, f"No results found for: {query}")
|
||||||
|
return
|
||||||
|
|
||||||
|
output = f"<strong>🔍 Shodan Search: '{query}'</strong><br>"
|
||||||
|
output += f"<strong>Total Results:</strong> {data.get('total', 0):,}<br><br>"
|
||||||
|
|
||||||
|
for match in data['matches'][:5]: # Show first 5 results
|
||||||
|
ip = match.get('ip_str', 'N/A')
|
||||||
|
port = match.get('port', 'N/A')
|
||||||
|
org = match.get('org', 'Unknown')
|
||||||
|
product = match.get('product', 'Unknown')
|
||||||
|
|
||||||
|
output += f"<strong>🌐 {ip}:{port}</strong><br>"
|
||||||
|
output += f" • <strong>Organization:</strong> {org}<br>"
|
||||||
|
output += f" • <strong>Service:</strong> {product}<br>"
|
||||||
|
|
||||||
|
if match.get('location'):
|
||||||
|
loc = match['location']
|
||||||
|
if loc.get('city') and loc.get('country_name'):
|
||||||
|
output += f" • <strong>Location:</strong> {loc['city']}, {loc['country_name']}<br>"
|
||||||
|
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
if data.get('total', 0) > 5:
|
||||||
|
output += f"<em>Showing 5 of {data['total']:,} results. Refine your search for more specific results.</em>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
logging.info(f"Sent Shodan search results for: {query}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
|
||||||
|
logging.error("Shodan API timeout")
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error searching Shodan: {str(e)}")
|
||||||
|
logging.error(f"Error in shodan_search: {e}")
|
||||||
|
|
||||||
|
async def shodan_host(room, bot, host):
|
||||||
|
"""Get host information (domain or IP)."""
|
||||||
|
try:
|
||||||
|
url = f"{SHODAN_API_BASE}/dns/domain/{host}"
|
||||||
|
params = {"key": SHODAN_API_KEY}
|
||||||
|
|
||||||
|
logging.info(f"Fetching Shodan host info for: {host}")
|
||||||
|
response = requests.get(url, params=params, timeout=15)
|
||||||
|
|
||||||
|
if response.status_code == 404:
|
||||||
|
# Try IP lookup instead
|
||||||
|
await shodan_ip_lookup(room, bot, host)
|
||||||
|
return
|
||||||
|
elif response.status_code != 200:
|
||||||
|
await handle_shodan_error(room, bot, response.status_code)
|
||||||
|
return
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
output = f"<strong>🔍 Shodan Host: {host}</strong><br><br>"
|
||||||
|
|
||||||
|
if data.get('subdomains'):
|
||||||
|
output += f"<strong>🌐 Subdomains ({len(data['subdomains'])}):</strong><br>"
|
||||||
|
for subdomain in sorted(data['subdomains'])[:10]: # Show first 10
|
||||||
|
output += f" • {subdomain}.{host}<br>"
|
||||||
|
|
||||||
|
if len(data['subdomains']) > 10:
|
||||||
|
output += f" • ... and {len(data['subdomains']) - 10} more<br>"
|
||||||
|
|
||||||
|
if data.get('tags'):
|
||||||
|
output += f"<br><strong>🏷️ Tags:</strong> {', '.join(data['tags'])}<br>"
|
||||||
|
|
||||||
|
if data.get('data'):
|
||||||
|
output += f"<br><strong>📊 Records Found:</strong> {len(data['data'])}<br>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
logging.info(f"Sent Shodan host info for: {host}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
|
||||||
|
logging.error("Shodan API timeout")
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error fetching host info: {str(e)}")
|
||||||
|
logging.error(f"Error in shodan_host: {e}")
|
||||||
|
|
||||||
|
async def shodan_count(room, bot, query):
|
||||||
|
"""Count results for a search query."""
|
||||||
|
try:
|
||||||
|
url = f"{SHODAN_API_BASE}/shodan/host/count"
|
||||||
|
params = {
|
||||||
|
"key": SHODAN_API_KEY,
|
||||||
|
"query": query
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f"Counting Shodan results for: {query}")
|
||||||
|
response = requests.get(url, params=params, timeout=15)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
await handle_shodan_error(room, bot, response.status_code)
|
||||||
|
return
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
output = f"<strong>🔍 Shodan Count: '{query}'</strong><br><br>"
|
||||||
|
output += f"<strong>Total Results:</strong> {data.get('total', 0):,}<br>"
|
||||||
|
|
||||||
|
# Show top countries if available
|
||||||
|
if data.get('facets') and 'country' in data['facets']:
|
||||||
|
output += "<br><strong>🌍 Top Countries:</strong><br>"
|
||||||
|
for country in data['facets']['country'][:5]:
|
||||||
|
output += f" • {country['value']}: {country['count']:,}<br>"
|
||||||
|
|
||||||
|
# Show top organizations if available
|
||||||
|
if data.get('facets') and 'org' in data['facets']:
|
||||||
|
output += "<br><strong>🏢 Top Organizations:</strong><br>"
|
||||||
|
for org in data['facets']['org'][:5]:
|
||||||
|
output += f" • {org['value']}: {org['count']:,}<br>"
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
logging.info(f"Sent Shodan count for: {query}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
|
||||||
|
logging.error("Shodan API timeout")
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error counting Shodan results: {str(e)}")
|
||||||
|
logging.error(f"Error in shodan_count: {e}")
|
||||||
|
|
||||||
|
async def handle_shodan_error(room, bot, status_code):
|
||||||
|
"""Handle Shodan API errors."""
|
||||||
|
error_messages = {
|
||||||
|
401: "Invalid Shodan API key",
|
||||||
|
403: "Access denied - check API key permissions",
|
||||||
|
404: "No results found",
|
||||||
|
429: "Rate limit exceeded - try again later",
|
||||||
|
500: "Shodan API server error",
|
||||||
|
503: "Shodan API temporarily unavailable"
|
||||||
|
}
|
||||||
|
|
||||||
|
message = error_messages.get(status_code, f"Shodan API error: {status_code}")
|
||||||
|
await bot.api.send_text_message(room.room_id, message)
|
||||||
|
logging.error(f"Shodan API error: {status_code}")
|
594
plugins/sslscan.py
Normal file
594
plugins/sslscan.py
Normal file
@@ -0,0 +1,594 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides comprehensive SSL/TLS security scanning and analysis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import OpenSSL
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
# SSL/TLS configuration - handle missing protocols in modern Python
|
||||||
|
TLS_VERSIONS = {
|
||||||
|
'TLSv1.2': ssl.PROTOCOL_TLSv1_2,
|
||||||
|
'TLSv1.3': ssl.PROTOCOL_TLS
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to add older protocols if available (they're removed in modern Python)
|
||||||
|
try:
|
||||||
|
TLS_VERSIONS['TLSv1.1'] = ssl.PROTOCOL_TLSv1_1
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
TLS_VERSIONS['TLSv1'] = ssl.PROTOCOL_TLSv1
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Cipher suites by strength and category
|
||||||
|
CIPHER_CATEGORIES = {
|
||||||
|
'STRONG': [
|
||||||
|
'TLS_AES_256_GCM_SHA384',
|
||||||
|
'TLS_CHACHA20_POLY1305_SHA256',
|
||||||
|
'TLS_AES_128_GCM_SHA256',
|
||||||
|
'ECDHE-RSA-AES256-GCM-SHA384',
|
||||||
|
'ECDHE-ECDSA-AES256-GCM-SHA384',
|
||||||
|
'ECDHE-RSA-CHACHA20-POLY1305',
|
||||||
|
'ECDHE-ECDSA-CHACHA20-POLY1305',
|
||||||
|
'DHE-RSA-AES256-GCM-SHA384'
|
||||||
|
],
|
||||||
|
'WEAK': [
|
||||||
|
'RC4',
|
||||||
|
'DES',
|
||||||
|
'3DES',
|
||||||
|
'MD5',
|
||||||
|
'EXPORT',
|
||||||
|
'NULL',
|
||||||
|
'ANON',
|
||||||
|
'ADH',
|
||||||
|
'CBC'
|
||||||
|
],
|
||||||
|
'OBSOLETE': [
|
||||||
|
'SSLv2',
|
||||||
|
'SSLv3'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle !sslscan command for comprehensive SSL/TLS analysis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("sslscan"):
|
||||||
|
logging.info("Received !sslscan command")
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
if len(args) < 1:
|
||||||
|
await show_usage(room, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
target = args[0].strip()
|
||||||
|
port = 443
|
||||||
|
|
||||||
|
# Parse port if provided
|
||||||
|
if ':' in target:
|
||||||
|
parts = target.split(':')
|
||||||
|
target = parts[0]
|
||||||
|
try:
|
||||||
|
port = int(parts[1])
|
||||||
|
except ValueError:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Invalid port number")
|
||||||
|
return
|
||||||
|
|
||||||
|
await perform_ssl_scan(room, bot, target, port)
|
||||||
|
|
||||||
|
async def show_usage(room, bot):
|
||||||
|
"""Display sslscan command usage."""
|
||||||
|
usage = """
|
||||||
|
<strong>🔐 SSL/TLS Security Scanner</strong>
|
||||||
|
|
||||||
|
<strong>!sslscan <domain[:port]></strong> - Comprehensive SSL/TLS security analysis
|
||||||
|
|
||||||
|
<strong>Examples:</strong>
|
||||||
|
• <code>!sslscan example.com</code>
|
||||||
|
• <code>!sslscan github.com:443</code>
|
||||||
|
• <code>!sslscan localhost:8443</code>
|
||||||
|
|
||||||
|
<strong>Tests Performed:</strong>
|
||||||
|
• SSL/TLS protocol support and versions
|
||||||
|
• Certificate chain validation and expiration
|
||||||
|
• Cipher suite strength and configuration
|
||||||
|
• Security headers and extensions
|
||||||
|
• Handshake simulation and vulnerabilities
|
||||||
|
• PCI DSS compliance checking
|
||||||
|
• SSL/TLS best practices assessment
|
||||||
|
"""
|
||||||
|
await bot.api.send_markdown_message(room.room_id, usage)
|
||||||
|
|
||||||
|
async def perform_ssl_scan(room, bot, target, port):
|
||||||
|
"""Perform comprehensive SSL/TLS security scan."""
|
||||||
|
try:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"🔍 Starting comprehensive SSL/TLS scan for {target}:{port}...")
|
||||||
|
|
||||||
|
scan_results = {
|
||||||
|
'target': target,
|
||||||
|
'port': port,
|
||||||
|
'certificate': {},
|
||||||
|
'protocols': {},
|
||||||
|
'ciphers': {},
|
||||||
|
'vulnerabilities': [],
|
||||||
|
'recommendations': [],
|
||||||
|
'security_score': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test basic connectivity
|
||||||
|
if not await test_connectivity(target, port):
|
||||||
|
await bot.api.send_text_message(room.room_id, f"❌ Cannot connect to {target}:{port}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Perform comprehensive tests
|
||||||
|
await get_certificate_info(scan_results, target, port)
|
||||||
|
await test_protocol_support(scan_results, target, port)
|
||||||
|
await test_cipher_suites(scan_results, target, port)
|
||||||
|
await check_vulnerabilities(scan_results)
|
||||||
|
await calculate_security_score(scan_results)
|
||||||
|
await generate_recommendations(scan_results)
|
||||||
|
|
||||||
|
# Format and send results
|
||||||
|
output = await format_ssl_scan_results(scan_results)
|
||||||
|
await bot.api.send_markdown_message(room.room_id, output)
|
||||||
|
|
||||||
|
logging.info(f"Completed SSL scan for {target}:{port}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error performing SSL scan: {str(e)}")
|
||||||
|
logging.error(f"Error in perform_ssl_scan: {e}")
|
||||||
|
|
||||||
|
async def test_connectivity(target, port):
|
||||||
|
"""Test basic connectivity to the target."""
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(10)
|
||||||
|
result = sock.connect_ex((target, port))
|
||||||
|
sock.close()
|
||||||
|
return result == 0
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_certificate_info(scan_results, target, port):
|
||||||
|
"""Get comprehensive certificate information."""
|
||||||
|
try:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
with socket.create_connection((target, port), timeout=10) as sock:
|
||||||
|
with context.wrap_socket(sock, server_hostname=target) as ssock:
|
||||||
|
cert_bin = ssock.getpeercert(binary_form=True)
|
||||||
|
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert_bin)
|
||||||
|
|
||||||
|
# Basic certificate info
|
||||||
|
subject = cert.get_subject()
|
||||||
|
issuer = cert.get_issuer()
|
||||||
|
|
||||||
|
scan_results['certificate'] = {
|
||||||
|
'subject': {
|
||||||
|
'common_name': subject.CN,
|
||||||
|
'organization': subject.O,
|
||||||
|
'organizational_unit': subject.OU,
|
||||||
|
'country': subject.C,
|
||||||
|
'state': subject.ST,
|
||||||
|
'locality': subject.L
|
||||||
|
},
|
||||||
|
'issuer': {
|
||||||
|
'common_name': issuer.CN,
|
||||||
|
'organization': issuer.O,
|
||||||
|
'organizational_unit': issuer.OU
|
||||||
|
},
|
||||||
|
'serial_number': cert.get_serial_number(),
|
||||||
|
'version': cert.get_version(),
|
||||||
|
'not_before': cert.get_notBefore().decode('utf-8'),
|
||||||
|
'not_after': cert.get_notAfter().decode('utf-8'),
|
||||||
|
'signature_algorithm': cert.get_signature_algorithm().decode('utf-8'),
|
||||||
|
'extensions': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse extensions
|
||||||
|
for i in range(cert.get_extension_count()):
|
||||||
|
ext = cert.get_extension(i)
|
||||||
|
scan_results['certificate']['extensions'].append({
|
||||||
|
'name': ext.get_short_name().decode('utf-8'),
|
||||||
|
'value': str(ext)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Calculate days until expiration
|
||||||
|
not_after = datetime.datetime.strptime(scan_results['certificate']['not_after'], '%Y%m%d%H%M%SZ')
|
||||||
|
days_until_expiry = (not_after - datetime.datetime.utcnow()).days
|
||||||
|
scan_results['certificate']['days_until_expiry'] = days_until_expiry
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
scan_results['certificate_error'] = str(e)
|
||||||
|
|
||||||
|
async def test_protocol_support(scan_results, target, port):
|
||||||
|
"""Test support for various SSL/TLS protocols."""
|
||||||
|
protocols = {
|
||||||
|
'SSLv2': False,
|
||||||
|
'SSLv3': False,
|
||||||
|
'TLSv1': False,
|
||||||
|
'TLSv1.1': False,
|
||||||
|
'TLSv1.2': False,
|
||||||
|
'TLSv1.3': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test available protocols
|
||||||
|
for protocol_name in protocols.keys():
|
||||||
|
try:
|
||||||
|
if protocol_name in TLS_VERSIONS:
|
||||||
|
context = ssl.SSLContext(TLS_VERSIONS[protocol_name])
|
||||||
|
else:
|
||||||
|
# For protocols not available in this Python version, assume False
|
||||||
|
protocols[protocol_name] = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
with socket.create_connection((target, port), timeout=5) as sock:
|
||||||
|
with context.wrap_socket(sock, server_hostname=target) as ssock:
|
||||||
|
protocols[protocol_name] = True
|
||||||
|
# Get negotiated protocol
|
||||||
|
if hasattr(ssock, 'version'):
|
||||||
|
scan_results['negotiated_protocol'] = ssock.version()
|
||||||
|
except:
|
||||||
|
protocols[protocol_name] = False
|
||||||
|
|
||||||
|
scan_results['protocols'] = protocols
|
||||||
|
|
||||||
|
async def test_cipher_suites(scan_results, target, port):
|
||||||
|
"""Test supported cipher suites."""
|
||||||
|
try:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
# Get default cipher suites
|
||||||
|
context.set_ciphers('ALL:COMPLEMENTOFALL')
|
||||||
|
|
||||||
|
with socket.create_connection((target, port), timeout=10) as sock:
|
||||||
|
with context.wrap_socket(sock, server_hostname=target) as ssock:
|
||||||
|
cipher = ssock.cipher()
|
||||||
|
scan_results['ciphers'] = {
|
||||||
|
'negotiated_cipher': cipher[0] if cipher else 'Unknown',
|
||||||
|
'supported_ciphers': await get_supported_ciphers(target, port),
|
||||||
|
'weak_ciphers': [],
|
||||||
|
'strong_ciphers': []
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
scan_results['cipher_error'] = str(e)
|
||||||
|
|
||||||
|
async def get_supported_ciphers(target, port):
|
||||||
|
"""Get list of supported cipher suites."""
|
||||||
|
supported_ciphers = []
|
||||||
|
|
||||||
|
# Test common cipher suites
|
||||||
|
test_ciphers = [
|
||||||
|
'ECDHE-RSA-AES256-GCM-SHA384',
|
||||||
|
'ECDHE-ECDSA-AES256-GCM-SHA384',
|
||||||
|
'ECDHE-RSA-AES256-SHA384',
|
||||||
|
'ECDHE-ECDSA-AES256-SHA384',
|
||||||
|
'ECDHE-RSA-AES256-SHA',
|
||||||
|
'ECDHE-ECDSA-AES256-SHA',
|
||||||
|
'AES256-GCM-SHA384',
|
||||||
|
'AES256-SHA256',
|
||||||
|
'AES256-SHA',
|
||||||
|
'CAMELLIA256-SHA',
|
||||||
|
'PSK-AES256-CBC-SHA',
|
||||||
|
'ECDHE-RSA-AES128-GCM-SHA256',
|
||||||
|
'ECDHE-ECDSA-AES128-GCM-SHA256',
|
||||||
|
'ECDHE-RSA-AES128-SHA256',
|
||||||
|
'ECDHE-ECDSA-AES128-SHA256',
|
||||||
|
'ECDHE-RSA-AES128-SHA',
|
||||||
|
'ECDHE-ECDSA-AES128-SHA',
|
||||||
|
'AES128-GCM-SHA256',
|
||||||
|
'AES128-SHA256',
|
||||||
|
'AES128-SHA',
|
||||||
|
'CAMELLIA128-SHA',
|
||||||
|
'PSK-AES128-CBC-SHA',
|
||||||
|
'DES-CBC3-SHA',
|
||||||
|
'RC4-SHA',
|
||||||
|
'RC4-MD5'
|
||||||
|
]
|
||||||
|
|
||||||
|
for cipher in test_ciphers:
|
||||||
|
try:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
context.set_ciphers(cipher)
|
||||||
|
|
||||||
|
with socket.create_connection((target, port), timeout=5) as sock:
|
||||||
|
with context.wrap_socket(sock, server_hostname=target) as ssock:
|
||||||
|
if ssock.cipher():
|
||||||
|
supported_ciphers.append(cipher)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return supported_ciphers
|
||||||
|
|
||||||
|
async def check_vulnerabilities(scan_results):
|
||||||
|
"""Check for common SSL/TLS vulnerabilities."""
|
||||||
|
vulnerabilities = []
|
||||||
|
|
||||||
|
# Check for weak protocols (these will be False in modern Python, which is good)
|
||||||
|
if scan_results['protocols'].get('SSLv2', False):
|
||||||
|
vulnerabilities.append({
|
||||||
|
'name': 'SSLv2 Support',
|
||||||
|
'severity': 'CRITICAL',
|
||||||
|
'description': 'SSLv2 is obsolete and contains critical vulnerabilities',
|
||||||
|
'cve': 'Multiple CVEs'
|
||||||
|
})
|
||||||
|
|
||||||
|
if scan_results['protocols'].get('SSLv3', False):
|
||||||
|
vulnerabilities.append({
|
||||||
|
'name': 'SSLv3 Support',
|
||||||
|
'severity': 'HIGH',
|
||||||
|
'description': 'SSLv3 is vulnerable to POODLE attack',
|
||||||
|
'cve': 'CVE-2014-3566'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check certificate expiration
|
||||||
|
cert = scan_results.get('certificate', {})
|
||||||
|
if cert.get('days_until_expiry', 0) < 30:
|
||||||
|
vulnerabilities.append({
|
||||||
|
'name': 'Certificate Expiring Soon',
|
||||||
|
'severity': 'MEDIUM',
|
||||||
|
'description': f"Certificate expires in {cert['days_until_expiry']} days",
|
||||||
|
'cve': 'N/A'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check for weak ciphers
|
||||||
|
supported_ciphers = scan_results.get('ciphers', {}).get('supported_ciphers', [])
|
||||||
|
weak_ciphers_found = []
|
||||||
|
|
||||||
|
for cipher in supported_ciphers:
|
||||||
|
if any(weak in cipher.upper() for weak in CIPHER_CATEGORIES['WEAK']):
|
||||||
|
weak_ciphers_found.append(cipher)
|
||||||
|
|
||||||
|
if weak_ciphers_found:
|
||||||
|
vulnerabilities.append({
|
||||||
|
'name': 'Weak Cipher Suites',
|
||||||
|
'severity': 'HIGH',
|
||||||
|
'description': f'Weak ciphers supported: {", ".join(weak_ciphers_found[:3])}',
|
||||||
|
'cve': 'Multiple CVEs'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check for missing modern protocols
|
||||||
|
if not scan_results['protocols'].get('TLSv1.2', False):
|
||||||
|
vulnerabilities.append({
|
||||||
|
'name': 'TLS 1.2 Not Supported',
|
||||||
|
'severity': 'HIGH',
|
||||||
|
'description': 'TLS 1.2 is required for modern security',
|
||||||
|
'cve': 'N/A'
|
||||||
|
})
|
||||||
|
|
||||||
|
if not scan_results['protocols'].get('TLSv1.3', False):
|
||||||
|
vulnerabilities.append({
|
||||||
|
'name': 'TLS 1.3 Not Supported',
|
||||||
|
'severity': 'MEDIUM',
|
||||||
|
'description': 'TLS 1.3 provides improved security and performance',
|
||||||
|
'cve': 'N/A'
|
||||||
|
})
|
||||||
|
|
||||||
|
scan_results['vulnerabilities'] = vulnerabilities
|
||||||
|
|
||||||
|
async def calculate_security_score(scan_results):
|
||||||
|
"""Calculate overall security score."""
|
||||||
|
score = 100
|
||||||
|
|
||||||
|
# Protocol penalties (in modern Python, SSLv2/SSLv3 will be False, which is good)
|
||||||
|
if scan_results['protocols'].get('SSLv2', False):
|
||||||
|
score -= 30
|
||||||
|
if scan_results['protocols'].get('SSLv3', False):
|
||||||
|
score -= 20
|
||||||
|
if not scan_results['protocols'].get('TLSv1.2', False):
|
||||||
|
score -= 15
|
||||||
|
if not scan_results['protocols'].get('TLSv1.3', False):
|
||||||
|
score -= 10
|
||||||
|
|
||||||
|
# Certificate penalties
|
||||||
|
cert = scan_results.get('certificate', {})
|
||||||
|
if cert.get('days_until_expiry', 0) < 30:
|
||||||
|
score -= 10
|
||||||
|
if cert.get('days_until_expiry', 0) < 7:
|
||||||
|
score -= 20
|
||||||
|
|
||||||
|
# Cipher penalties
|
||||||
|
supported_ciphers = scan_results.get('ciphers', {}).get('supported_ciphers', [])
|
||||||
|
weak_cipher_count = sum(1 for cipher in supported_ciphers
|
||||||
|
if any(weak in cipher.upper() for weak in CIPHER_CATEGORIES['WEAK']))
|
||||||
|
score -= min(weak_cipher_count * 5, 25)
|
||||||
|
|
||||||
|
# Vulnerability penalties
|
||||||
|
for vuln in scan_results.get('vulnerabilities', []):
|
||||||
|
if vuln['severity'] == 'CRITICAL':
|
||||||
|
score -= 20
|
||||||
|
elif vuln['severity'] == 'HIGH':
|
||||||
|
score -= 15
|
||||||
|
elif vuln['severity'] == 'MEDIUM':
|
||||||
|
score -= 10
|
||||||
|
elif vuln['severity'] == 'LOW':
|
||||||
|
score -= 5
|
||||||
|
|
||||||
|
scan_results['security_score'] = max(0, score)
|
||||||
|
|
||||||
|
async def generate_recommendations(scan_results):
|
||||||
|
"""Generate security recommendations."""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
# Protocol recommendations
|
||||||
|
if scan_results['protocols'].get('SSLv2', False):
|
||||||
|
recommendations.append("🔴 IMMEDIATELY disable SSLv2 - critically vulnerable")
|
||||||
|
if scan_results['protocols'].get('SSLv3', False):
|
||||||
|
recommendations.append("🔴 Disable SSLv3 - vulnerable to POODLE attack")
|
||||||
|
if not scan_results['protocols'].get('TLSv1.3', False):
|
||||||
|
recommendations.append("🟡 Enable TLSv1.3 for best security and performance")
|
||||||
|
|
||||||
|
# Certificate recommendations
|
||||||
|
cert = scan_results.get('certificate', {})
|
||||||
|
if cert.get('days_until_expiry', 0) < 30:
|
||||||
|
recommendations.append("🟡 Renew SSL certificate - expiring soon")
|
||||||
|
|
||||||
|
# Cipher recommendations
|
||||||
|
supported_ciphers = scan_results.get('ciphers', {}).get('supported_ciphers', [])
|
||||||
|
weak_ciphers = [c for c in supported_ciphers
|
||||||
|
if any(weak in c.upper() for weak in CIPHER_CATEGORIES['WEAK'])]
|
||||||
|
|
||||||
|
if weak_ciphers:
|
||||||
|
recommendations.append("🔴 Remove weak cipher suites (RC4, DES, 3DES, NULL)")
|
||||||
|
|
||||||
|
# General recommendations
|
||||||
|
if scan_results['security_score'] < 80:
|
||||||
|
recommendations.append("🛡️ Implement modern TLS configuration following Mozilla guidelines")
|
||||||
|
|
||||||
|
if not any('ECDHE' in c for c in supported_ciphers):
|
||||||
|
recommendations.append("🟡 Enable Forward Secrecy with ECDHE cipher suites")
|
||||||
|
|
||||||
|
# Add note about Python version limitations
|
||||||
|
recommendations.append("ℹ️ Note: SSLv2/SSLv3 testing limited by Python security features")
|
||||||
|
|
||||||
|
scan_results['recommendations'] = recommendations
|
||||||
|
|
||||||
|
async def format_ssl_scan_results(scan_results):
|
||||||
|
"""Format comprehensive SSL scan results."""
|
||||||
|
output = f"<strong>🔐 SSL/TLS Security Scan: {scan_results['target']}:{scan_results['port']}</strong><br><br>"
|
||||||
|
|
||||||
|
# Security Score
|
||||||
|
score = scan_results['security_score']
|
||||||
|
if score >= 90:
|
||||||
|
score_emoji, rating = "🟢", "Excellent"
|
||||||
|
elif score >= 80:
|
||||||
|
score_emoji, rating = "🟡", "Good"
|
||||||
|
elif score >= 60:
|
||||||
|
score_emoji, rating = "🟠", "Fair"
|
||||||
|
else:
|
||||||
|
score_emoji, rating = "🔴", "Poor"
|
||||||
|
|
||||||
|
output += f"<strong>{score_emoji} Security Score: {score}/100 ({rating})</strong><br><br>"
|
||||||
|
|
||||||
|
# Certificate Information
|
||||||
|
cert = scan_results.get('certificate', {})
|
||||||
|
if cert:
|
||||||
|
output += "<strong>📜 Certificate Information</strong><br>"
|
||||||
|
output += f" • <strong>Subject:</strong> {cert.get('subject', {}).get('common_name', 'N/A')}<br>"
|
||||||
|
output += f" • <strong>Issuer:</strong> {cert.get('issuer', {}).get('common_name', 'N/A')}<br>"
|
||||||
|
output += f" • <strong>Valid From:</strong> {format_cert_date(cert.get('not_before', ''))}<br>"
|
||||||
|
output += f" • <strong>Valid Until:</strong> {format_cert_date(cert.get('not_after', ''))}<br>"
|
||||||
|
output += f" • <strong>Expires In:</strong> {cert.get('days_until_expiry', 'N/A')} days<br>"
|
||||||
|
output += f" • <strong>Signature Algorithm:</strong> {cert.get('signature_algorithm', 'N/A')}<br>"
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Protocol Support
|
||||||
|
output += "<strong>🔌 Protocol Support</strong><br>"
|
||||||
|
protocols = scan_results.get('protocols', {})
|
||||||
|
for proto, supported in protocols.items():
|
||||||
|
# Handle protocols that can't be tested in this Python version
|
||||||
|
if proto in ['SSLv2', 'SSLv3'] and proto not in TLS_VERSIONS:
|
||||||
|
emoji = "⚫"
|
||||||
|
status = "Cannot test (Python security)"
|
||||||
|
else:
|
||||||
|
emoji = "✅" if supported else "❌"
|
||||||
|
|
||||||
|
# Highlight insecure protocols
|
||||||
|
if proto in ['SSLv2', 'SSLv3'] and supported:
|
||||||
|
emoji = "🔴"
|
||||||
|
elif proto in ['TLSv1.3'] and supported:
|
||||||
|
emoji = "✅"
|
||||||
|
|
||||||
|
output += f" • {emoji} <strong>{proto}:</strong> {status if 'status' in locals() else 'Supported' if supported else 'Not Supported'}<br>"
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Cipher Information
|
||||||
|
ciphers = scan_results.get('ciphers', {})
|
||||||
|
if ciphers.get('supported_ciphers'):
|
||||||
|
output += "<strong>🔐 Cipher Suites</strong><br>"
|
||||||
|
output += f" • <strong>Negotiated:</strong> {ciphers.get('negotiated_cipher', 'Unknown')}<br>"
|
||||||
|
output += f" • <strong>Total Supported:</strong> {len(ciphers['supported_ciphers'])}<br>"
|
||||||
|
|
||||||
|
# Show weak ciphers if any
|
||||||
|
weak_ciphers = [c for c in ciphers['supported_ciphers']
|
||||||
|
if any(weak in c.upper() for weak in CIPHER_CATEGORIES['WEAK'])]
|
||||||
|
if weak_ciphers:
|
||||||
|
output += f" • <strong>Weak Ciphers:</strong> {len(weak_ciphers)} found<br>"
|
||||||
|
for cipher in weak_ciphers[:3]:
|
||||||
|
output += f" └─ 🔴 {cipher}<br>"
|
||||||
|
|
||||||
|
# Show strong ciphers if any
|
||||||
|
strong_ciphers = [c for c in ciphers['supported_ciphers']
|
||||||
|
if any(strong in c.upper() for strong in CIPHER_CATEGORIES['STRONG'])]
|
||||||
|
if strong_ciphers:
|
||||||
|
output += f" • <strong>Strong Ciphers:</strong> {len(strong_ciphers)} found<br>"
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Vulnerabilities
|
||||||
|
vulnerabilities = scan_results.get('vulnerabilities', [])
|
||||||
|
if vulnerabilities:
|
||||||
|
output += "<strong>⚠️ Security Vulnerabilities</strong><br>"
|
||||||
|
for vuln in vulnerabilities[:5]: # Show top 5
|
||||||
|
severity_emoji = "🔴" if vuln['severity'] == 'CRITICAL' else "🟠" if vuln['severity'] == 'HIGH' else "🟡"
|
||||||
|
output += f" • {severity_emoji} <strong>{vuln['name']}</strong> ({vuln['severity']})<br>"
|
||||||
|
output += f" └─ {vuln['description']}<br>"
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Recommendations
|
||||||
|
recommendations = scan_results.get('recommendations', [])
|
||||||
|
if recommendations:
|
||||||
|
output += "<strong>💡 Security Recommendations</strong><br>"
|
||||||
|
for rec in recommendations[:8]:
|
||||||
|
output += f" • {rec}<br>"
|
||||||
|
output += "<br>"
|
||||||
|
|
||||||
|
# Quick Assessment
|
||||||
|
output += "<strong>📊 Quick Assessment</strong><br>"
|
||||||
|
if score >= 90:
|
||||||
|
output += " • ✅ Excellent TLS configuration<br>"
|
||||||
|
output += " • ✅ Modern protocols and ciphers<br>"
|
||||||
|
output += " • ✅ Good certificate management<br>"
|
||||||
|
elif score >= 70:
|
||||||
|
output += " • ⚠️ Good configuration with minor issues<br>"
|
||||||
|
output += " • 🔧 Some improvements recommended<br>"
|
||||||
|
else:
|
||||||
|
output += " • 🚨 Significant security issues found<br>"
|
||||||
|
output += " • 🔴 Immediate action required<br>"
|
||||||
|
|
||||||
|
# Add note about testing limitations
|
||||||
|
output += "<br><em>ℹ️ Note: Some protocol tests limited by Python security features</em>"
|
||||||
|
|
||||||
|
# Always wrap in collapsible due to comprehensive output
|
||||||
|
output = f"<details><summary><strong>🔐 SSL/TLS Scan: {scan_results['target']}:{scan_results['port']} (Score: {score}/100)</strong></summary>{output}</details>"
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def format_cert_date(date_str):
|
||||||
|
"""Format certificate date string for display."""
|
||||||
|
try:
|
||||||
|
if date_str:
|
||||||
|
dt = datetime.datetime.strptime(date_str, '%Y%m%d%H%M%SZ')
|
||||||
|
return dt.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return date_str
|
@@ -1,77 +1,156 @@
|
|||||||
"""
|
"""
|
||||||
This plugin provides a command to generate images using self hosted Stable Diffusion and send to the room
|
Plugin for generating images using self-hosted Stable Diffusion and sending them to a Matrix chat room.
|
||||||
"""
|
"""
|
||||||
# plugins/stable-diffusion.py
|
|
||||||
import requests
|
import requests
|
||||||
import base64
|
import base64
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
from asyncio import Queue
|
from asyncio import Queue
|
||||||
import argparse
|
import argparse
|
||||||
import simplematrixbotlib as botlib
|
import simplematrixbotlib as botlib
|
||||||
import markdown2
|
import markdown2
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
# Queue to store pending commands
|
# Queue to store pending commands
|
||||||
command_queue = Queue()
|
command_queue = Queue()
|
||||||
|
|
||||||
def markdown_to_html(markdown_text):
|
def slugify_prompt(prompt: str) -> str:
|
||||||
html_content = markdown2.markdown(markdown_text)
|
"""
|
||||||
return html_content
|
Generates a URL-friendly slug from the given prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt (str): The prompt to slugify.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A URL-friendly slug version of the prompt.
|
||||||
|
"""
|
||||||
|
return slugify(prompt)
|
||||||
|
|
||||||
|
def markdown_to_html(markdown_text: str) -> str:
|
||||||
|
"""
|
||||||
|
Converts Markdown text to HTML.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
markdown_text (str): The Markdown text to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The HTML version of the input Markdown text.
|
||||||
|
"""
|
||||||
|
return markdown2.markdown(markdown_text)
|
||||||
|
|
||||||
async def process_command(room, message, bot, prefix, config):
|
async def process_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Processes !sd commands and queues them if already running.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room: Matrix room object
|
||||||
|
message: Matrix message object
|
||||||
|
bot: Bot instance
|
||||||
|
prefix: Command prefix
|
||||||
|
config: Bot config object
|
||||||
|
"""
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
if match.prefix() and match.command("sd"):
|
if match.prefix() and match.command("sd"):
|
||||||
if command_queue.empty():
|
if command_queue.empty():
|
||||||
await handle_command(room, message, bot, prefix, config)
|
await handle_command(room, message, bot, prefix, config)
|
||||||
else:
|
else:
|
||||||
await command_queue.put((room, message, bot, prefix, config))
|
await command_queue.put((room, message, bot, prefix, config))
|
||||||
|
await bot.api.send_text_message(room.room_id, "Command queued. Please wait for the current image to finish.")
|
||||||
|
|
||||||
async def handle_command(room, message, bot, prefix, config):
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Handles !sd command: generates image using Stable Diffusion API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room: Matrix room object
|
||||||
|
message: Matrix message object
|
||||||
|
bot: Bot instance
|
||||||
|
prefix: Command prefix
|
||||||
|
config: Bot config object
|
||||||
|
"""
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
if match.prefix() and match.command("sd"):
|
if not (match.prefix() and match.command("sd")):
|
||||||
try:
|
return
|
||||||
parser = argparse.ArgumentParser(description='Generate images using self hosted Stable Diffusion')
|
|
||||||
parser.add_argument('--steps', type=int, default=4, help='Number of steps, default=16')
|
|
||||||
parser.add_argument('--cfg', type=int, default=1.25, help='CFG scale, default=7')
|
|
||||||
parser.add_argument('--h', type=int, default=512, help='Height of the image, default=512')
|
|
||||||
parser.add_argument('--w', type=int, default=512, help='Width of the image, default=512')
|
|
||||||
parser.add_argument('--neg', type=str, default='((((ugly)))), (((duplicate))), ((morbid)), ((mutilated)), out of frame, extra fingers, mutated hands, ((poorly drawn hands)), ((poorly drawn face)), (((mutation))), (((deformed))), ((ugly)), blurry, ((bad anatomy)), (((bad proportions))), ((extra limbs)), cloned face, (((disfigured))), out of frame, ugly, extra limbs, (bad anatomy), gross proportions, (malformed limbs), ((missing arms)), ((missing legs)), (((extra arms))), (((extra legs))), mutated hands, (fused fingers), (too many fingers), (((long neck)))', nargs='+', help='Negative prompt, default=none')
|
|
||||||
parser.add_argument('--sampler', type=str, nargs='*', default=['DPM++', 'SDE', 'Karras'], help='Sampler name, default=Euler a')
|
|
||||||
parser.add_argument('prompt', type=str, nargs='*', help='Prompt for the image')
|
|
||||||
|
|
||||||
args = parser.parse_args(message.body.split()[1:]) # Skip the command itself
|
# Check if API is available
|
||||||
|
try:
|
||||||
|
health_check = requests.get("http://127.0.0.1:7860/docs", timeout=3)
|
||||||
|
if health_check.status_code != 200:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Stable Diffusion API is not running!")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
await bot.api.send_text_message(room.room_id, "Could not reach Stable Diffusion API!")
|
||||||
|
return
|
||||||
|
|
||||||
if not args.prompt:
|
try:
|
||||||
raise argparse.ArgumentError(None, "Prompt is required.")
|
# Parse command-line arguments
|
||||||
|
parser = argparse.ArgumentParser(description='Generate images using self-hosted Stable Diffusion')
|
||||||
|
parser.add_argument('--steps', type=int, default=4, help='Number of steps, default=4')
|
||||||
|
parser.add_argument('--cfg', type=int, default=2, help='CFG scale, default=2')
|
||||||
|
parser.add_argument('--h', type=int, default=512, help='Height of the image, default=512')
|
||||||
|
parser.add_argument('--w', type=int, default=512, help='Width of the image, default=512')
|
||||||
|
parser.add_argument('--neg', type=str, nargs='+', default=['((((ugly)))), (((duplicate))), ((morbid)), ((mutilated)), out of frame, extra fingers, mutated hands, ((poorly drawn hands)), ((poorly drawn face)), (((mutation))), (((deformed))), ((ugly)), blurry, ((bad anatomy)), (((bad proportions))), ((extra limbs)), cloned face, (((disfigured))), out of frame, ugly, extra limbs, (bad anatomy), gross proportions, (malformed limbs), ((missing arms)), ((missing legs)), (((extra arms))), (((extra legs))), mutated hands, (fused fingers), (too many fingers), (((long neck)))'], help='Negative prompt')
|
||||||
|
parser.add_argument('--sampler', type=str, nargs='*', default=['DPM++', 'SDE'], help='Sampler name, default=DPM++ SDE')
|
||||||
|
parser.add_argument('prompt', type=str, nargs='*', help='Prompt for the image')
|
||||||
|
|
||||||
prompt = ' '.join(args.prompt)
|
args = parser.parse_args(message.body.split()[1:]) # skip command prefix
|
||||||
sampler_name = ' '.join(args.sampler)
|
|
||||||
neg = ' '.join(args.neg)
|
|
||||||
payload = {
|
|
||||||
"prompt": prompt,
|
|
||||||
"steps": args.steps,
|
|
||||||
"negative_prompt": neg,
|
|
||||||
"sampler_name": sampler_name,
|
|
||||||
"cfg_scale": args.cfg,
|
|
||||||
"width": args.w,
|
|
||||||
"height": args.h,
|
|
||||||
}
|
|
||||||
url = "http://127.0.0.1:7860/sdapi/v1/txt2img"
|
|
||||||
|
|
||||||
response = requests.post(url=url, json=payload)
|
if not args.prompt:
|
||||||
r = response.json()
|
raise argparse.ArgumentError(None, "Prompt is required.")
|
||||||
with open("/tmp/output.jpg", 'wb') as f:
|
|
||||||
f.write(base64.b64decode(r['images'][0]))
|
prompt = ' '.join(args.prompt)
|
||||||
await bot.api.send_image_message(room_id=room.room_id, image_filepath="/tmp/output.jpg") # Corrected argument name
|
sampler_name = ' '.join(args.sampler)
|
||||||
except argparse.ArgumentError as e:
|
neg_prompt = ' '.join(args.neg)
|
||||||
await bot.api.send_text_message(room.room_id, f"Error: {e}")
|
|
||||||
await bot.api.send_markdown_message(room.room_id, "<details><summary>Stable Diffusion Help</summary>" + print_help() + "</details>")
|
payload = {
|
||||||
except Exception as e:
|
"prompt": prompt,
|
||||||
await bot.api.send_text_message(room.room_id, f"Error processing the command: {str(e)}")
|
"steps": args.steps,
|
||||||
finally:
|
"negative_prompt": neg_prompt,
|
||||||
if not command_queue.empty():
|
"sampler_name": sampler_name,
|
||||||
next_command = await command_queue.get()
|
"cfg_scale": args.cfg,
|
||||||
await handle_command(*next_command)
|
"width": args.w,
|
||||||
|
"height": args.h,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = "http://127.0.0.1:7860/sdapi/v1/txt2img"
|
||||||
|
response = requests.post(url=url, json=payload, timeout=600)
|
||||||
|
r = response.json()
|
||||||
|
|
||||||
|
# Use secure temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as temp_file:
|
||||||
|
filename = temp_file.name
|
||||||
|
temp_file.write(base64.b64decode(r['images'][0]))
|
||||||
|
|
||||||
|
# Send image to Matrix room
|
||||||
|
await bot.api.send_image_message(room_id=room.room_id, image_filepath=filename)
|
||||||
|
|
||||||
|
# Optional: send info about generated image
|
||||||
|
neg_prompt_clean = neg_prompt.replace(" ", "")
|
||||||
|
info_msg = f"""<details><summary>🔍 Image Info</summary><strong>Prompt:</strong> {prompt[:100]}<br><strong>Steps:</strong> {args.steps}<br><strong>Dimensions:</strong> {args.h}x{args.w}<br><strong>Sampler:</strong> {sampler_name}<br><strong>CFG Scale:</strong> {args.cfg}<br><strong>Negative Prompt:</strong> {neg_prompt_clean}</details>"""
|
||||||
|
# await bot.api.send_markdown_message(room.room_id, info_msg)
|
||||||
|
|
||||||
|
# Clean up temp file
|
||||||
|
os.remove(filename)
|
||||||
|
|
||||||
|
except argparse.ArgumentError as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Argument Error: {e}")
|
||||||
|
await bot.api.send_markdown_message(room.room_id, "<details><summary>Stable Diffusion Help</summary>" + print_help() + "</details>")
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error processing the command: {str(e)}")
|
||||||
|
finally:
|
||||||
|
# Process next queued command
|
||||||
|
if not command_queue.empty():
|
||||||
|
next_command = await command_queue.get()
|
||||||
|
await handle_command(*next_command)
|
||||||
|
|
||||||
def print_help():
|
def print_help():
|
||||||
|
"""
|
||||||
|
Generates help text for the 'sd' command.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Help text for the 'sd' command.
|
||||||
|
"""
|
||||||
return """
|
return """
|
||||||
<p>Generate images using self-hosted Stable Diffusion</p>
|
<p>Generate images using self-hosted Stable Diffusion</p>
|
||||||
|
|
||||||
@@ -80,6 +159,8 @@ def print_help():
|
|||||||
<li>prompt - Prompt for the image</li>
|
<li>prompt - Prompt for the image</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<p>Default Negative Prompts: ((((ugly)))), (((duplicate))), ((morbid)), ((mutilated)), out of frame, extra fingers, mutated hands, ((poorly drawn hands)), ((poorly drawn face)), (((mutation))), (((deformed))), ((ugly)), blurry, ((bad anatomy)), (((bad proportions))), ((extra limbs)), cloned face, (((disfigured))), out of frame, ugly, extra limbs, (bad anatomy), gross proportions, (malformed limbs), ((missing arms)), ((missing legs)), (((extra arms))), (((extra legs))), mutated hands, (fused fingers), (too many fingers), (((long neck)))</p>
|
||||||
|
|
||||||
<p>Optional arguments:</p>
|
<p>Optional arguments:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>--steps STEPS - Number of steps, default=16</li>
|
<li>--steps STEPS - Number of steps, default=16</li>
|
||||||
|
193
plugins/urbandictionary.py
Normal file
193
plugins/urbandictionary.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides a command to fetch definitions from Urban Dictionary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
import html
|
||||||
|
|
||||||
|
URBAN_API_URL = "https://api.urbandictionary.com/v0/define"
|
||||||
|
RANDOM_API_URL = "https://api.urbandictionary.com/v0/random"
|
||||||
|
|
||||||
|
|
||||||
|
def format_definition(term, definition, example, author, thumbs_up, thumbs_down, permalink, index=None, total=None):
|
||||||
|
"""
|
||||||
|
Format an Urban Dictionary definition for display.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
term (str): The term being defined.
|
||||||
|
definition (str): The definition text.
|
||||||
|
example (str): Example usage.
|
||||||
|
author (str): Author of the definition.
|
||||||
|
thumbs_up (int): Number of upvotes.
|
||||||
|
thumbs_down (int): Number of downvotes.
|
||||||
|
permalink (str): URL to the definition.
|
||||||
|
index (int, optional): Current definition index.
|
||||||
|
total (int, optional): Total number of definitions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted HTML message.
|
||||||
|
"""
|
||||||
|
# Clean up the text - Urban Dictionary uses [brackets] for links
|
||||||
|
definition = definition.replace('[', '<strong>').replace(']', '</strong>')
|
||||||
|
example = example.replace('[', '<em>').replace(']', '</em>')
|
||||||
|
|
||||||
|
# Escape any HTML that might be in the original text
|
||||||
|
term = html.escape(term)
|
||||||
|
author = html.escape(author)
|
||||||
|
|
||||||
|
# Build the message
|
||||||
|
header = f"<strong>📖 Urban Dictionary: {term}</strong>"
|
||||||
|
if index is not None and total is not None:
|
||||||
|
header += f" (Definition {index}/{total})"
|
||||||
|
|
||||||
|
message = f"""{header}
|
||||||
|
<strong>Definition:</strong>
|
||||||
|
{definition}
|
||||||
|
"""
|
||||||
|
if example and example.strip():
|
||||||
|
message += f"""
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<em>{example}</em>
|
||||||
|
"""
|
||||||
|
message += f"""
|
||||||
|
<strong>Author:</strong> {author} | 👍 {thumbs_up} 👎 {thumbs_down}
|
||||||
|
<a href="{permalink}">View on Urban Dictionary</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle the !ud (Urban Dictionary) command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("ud"):
|
||||||
|
logging.info("Received !ud command")
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Case 1: No arguments - get random definition
|
||||||
|
if len(args) == 0:
|
||||||
|
logging.info("Fetching random Urban Dictionary definition")
|
||||||
|
response = requests.get(RANDOM_API_URL, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
if not data.get('list'):
|
||||||
|
await bot.api.send_text_message(room.room_id, "No random definition found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get first random entry
|
||||||
|
entry = data['list'][0]
|
||||||
|
formatted = format_definition(
|
||||||
|
term=entry['word'],
|
||||||
|
definition=entry['definition'],
|
||||||
|
example=entry.get('example', ''),
|
||||||
|
author=entry['author'],
|
||||||
|
thumbs_up=entry['thumbs_up'],
|
||||||
|
thumbs_down=entry['thumbs_down'],
|
||||||
|
permalink=entry['permalink']
|
||||||
|
)
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, formatted)
|
||||||
|
logging.info(f"Sent random definition: {entry['word']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Case 2: One or more arguments - search for term
|
||||||
|
# Check if last argument is a number (definition index)
|
||||||
|
index = None
|
||||||
|
search_term = ' '.join(args)
|
||||||
|
|
||||||
|
if args[-1].isdigit():
|
||||||
|
index = int(args[-1])
|
||||||
|
search_term = ' '.join(args[:-1])
|
||||||
|
|
||||||
|
if not search_term:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Usage: !ud [term] [index]\nExamples:\n !ud - random definition\n !ud yeet - first definition of 'yeet'\n !ud yeet 2 - second definition of 'yeet'"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info(f"Searching Urban Dictionary for: {search_term}")
|
||||||
|
params = {'term': search_term}
|
||||||
|
response = requests.get(URBAN_API_URL, params=params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
definitions = data.get('list', [])
|
||||||
|
|
||||||
|
if not definitions:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"No definition found for '{search_term}'"
|
||||||
|
)
|
||||||
|
logging.info(f"No definition found for: {search_term}")
|
||||||
|
return
|
||||||
|
|
||||||
|
total = len(definitions)
|
||||||
|
|
||||||
|
# If no index specified, use first definition
|
||||||
|
if index is None:
|
||||||
|
index = 1
|
||||||
|
|
||||||
|
# Validate index
|
||||||
|
if index < 1 or index > total:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Invalid index. '{search_term}' has {total} definition(s). Use !ud {search_term} [1-{total}]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the requested definition (convert to 0-based index)
|
||||||
|
entry = definitions[index - 1]
|
||||||
|
|
||||||
|
formatted = format_definition(
|
||||||
|
term=entry['word'],
|
||||||
|
definition=entry['definition'],
|
||||||
|
example=entry.get('example', ''),
|
||||||
|
author=entry['author'],
|
||||||
|
thumbs_up=entry['thumbs_up'],
|
||||||
|
thumbs_down=entry['thumbs_down'],
|
||||||
|
permalink=entry['permalink'],
|
||||||
|
index=index,
|
||||||
|
total=total
|
||||||
|
)
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, formatted)
|
||||||
|
logging.info(f"Sent definition {index}/{total} for: {search_term}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Request timed out. Urban Dictionary may be slow or unavailable."
|
||||||
|
)
|
||||||
|
logging.error("Urban Dictionary API timeout")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Error fetching from Urban Dictionary: {e}"
|
||||||
|
)
|
||||||
|
logging.error(f"Error fetching from Urban Dictionary: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"An error occurred while processing the Urban Dictionary request."
|
||||||
|
)
|
||||||
|
logging.error(f"Unexpected error in Urban Dictionary plugin: {e}", exc_info=True)
|
162
plugins/weather.py
Normal file
162
plugins/weather.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
This plugin provides a command to get weather information for a location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file in the parent directory
|
||||||
|
# Get the directory where this plugin file is located
|
||||||
|
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# Get the parent directory (main bot directory)
|
||||||
|
parent_dir = os.path.dirname(plugin_dir)
|
||||||
|
# Load .env from parent directory
|
||||||
|
dotenv_path = os.path.join(parent_dir, '.env')
|
||||||
|
load_dotenv(dotenv_path)
|
||||||
|
|
||||||
|
# OpenWeatherMap API configuration
|
||||||
|
# Get your free API key from: https://openweathermap.org/api
|
||||||
|
WEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
|
||||||
|
WEATHER_API_URL = "https://api.openweathermap.org/data/2.5/weather"
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Function to handle the !weather command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (Bot): The bot object.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): Configuration parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.is_not_from_this_bot() and match.prefix() and match.command("weather"):
|
||||||
|
logging.info("Received !weather command")
|
||||||
|
|
||||||
|
# Check if API key is configured
|
||||||
|
if not WEATHER_API_KEY:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Weather API key not configured. Please set OPENWEATHER_API_KEY environment variable."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
args = match.args()
|
||||||
|
|
||||||
|
# Check if location was provided
|
||||||
|
if len(args) < 1:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Usage: !weather <location>\nExample: !weather London or !weather New York,US"
|
||||||
|
)
|
||||||
|
logging.info("Sent usage message for !weather")
|
||||||
|
return
|
||||||
|
|
||||||
|
location = ' '.join(args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Make API request to OpenWeatherMap
|
||||||
|
params = {
|
||||||
|
'q': location,
|
||||||
|
'appid': WEATHER_API_KEY,
|
||||||
|
'units': 'metric' # Use metric units (Celsius)
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(WEATHER_API_URL, params=params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
weather_data = response.json()
|
||||||
|
|
||||||
|
# Extract relevant weather information
|
||||||
|
city_name = weather_data['name']
|
||||||
|
country = weather_data['sys']['country']
|
||||||
|
temp = weather_data['main']['temp']
|
||||||
|
feels_like = weather_data['main']['feels_like']
|
||||||
|
humidity = weather_data['main']['humidity']
|
||||||
|
description = weather_data['weather'][0]['description'].capitalize()
|
||||||
|
wind_speed = weather_data['wind'].get('speed', 0)
|
||||||
|
|
||||||
|
# Convert temperature to Fahrenheit for display
|
||||||
|
temp_f = (temp * 9/5) + 32
|
||||||
|
feels_like_f = (feels_like * 9/5) + 32
|
||||||
|
|
||||||
|
# Get weather emoji based on condition
|
||||||
|
weather_emoji = get_weather_emoji(weather_data['weather'][0]['main'])
|
||||||
|
|
||||||
|
# Format the weather message
|
||||||
|
weather_message = f"""
|
||||||
|
<strong>[{weather_emoji} Weather for {city_name}, {country}]</strong>: <strong>Condition:</strong> {description} | <strong>Temperature:</strong> {temp:.1f}°C ({temp_f:.1f}°F) | <strong>Feels like:</strong> {feels_like:.1f}°C ({feels_like_f:.1f}°F) | <strong>Humidity:</strong> {humidity}% | <strong>Wind Speed:</strong> {wind_speed} m/s
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
await bot.api.send_markdown_message(room.room_id, weather_message)
|
||||||
|
logging.info(f"Sent weather information for {city_name}")
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if e.response.status_code == 404:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Location '{location}' not found. Please check the spelling and try again."
|
||||||
|
)
|
||||||
|
elif e.response.status_code == 401:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Weather API authentication failed. Please check the API key configuration."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Error fetching weather data: HTTP {e.response.status_code}"
|
||||||
|
)
|
||||||
|
logging.error(f"HTTP error fetching weather for '{location}': {e}")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
f"Error connecting to weather service: {e}"
|
||||||
|
)
|
||||||
|
logging.error(f"Request error fetching weather for '{location}': {e}")
|
||||||
|
|
||||||
|
except (KeyError, ValueError) as e:
|
||||||
|
await bot.api.send_text_message(
|
||||||
|
room.room_id,
|
||||||
|
"Error parsing weather data. Please try again later."
|
||||||
|
)
|
||||||
|
logging.error(f"Error parsing weather data for '{location}': {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_weather_emoji(condition):
|
||||||
|
"""
|
||||||
|
Get an emoji based on weather condition.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
condition (str): Weather condition from API.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Weather emoji.
|
||||||
|
"""
|
||||||
|
weather_emojis = {
|
||||||
|
'Clear': '☀️',
|
||||||
|
'Clouds': '☁️',
|
||||||
|
'Rain': '🌧️',
|
||||||
|
'Drizzle': '🌦️',
|
||||||
|
'Thunderstorm': '⛈️',
|
||||||
|
'Snow': '❄️',
|
||||||
|
'Mist': '🌫️',
|
||||||
|
'Fog': '🌫️',
|
||||||
|
'Haze': '🌫️',
|
||||||
|
'Smoke': '🌫️',
|
||||||
|
'Dust': '🌫️',
|
||||||
|
'Sand': '🌫️',
|
||||||
|
'Ash': '🌫️',
|
||||||
|
'Squall': '💨',
|
||||||
|
'Tornado': '🌪️'
|
||||||
|
}
|
||||||
|
|
||||||
|
return weather_emojis.get(condition, '🌡️')
|
45
plugins/xkcd.py
Normal file
45
plugins/xkcd.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Provides a command to fetch random xkcd comic
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import tempfile
|
||||||
|
import random
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
|
||||||
|
# Define the XKCD API URL
|
||||||
|
XKCD_API_URL = "https://xkcd.com/info.0.json"
|
||||||
|
|
||||||
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
|
if match.prefix() and match.command("xkcd"):
|
||||||
|
# Fetch the latest comic number from XKCD API
|
||||||
|
try:
|
||||||
|
response = requests.get(XKCD_API_URL, timeout=10)
|
||||||
|
response.raise_for_status() # Raise an exception for non-200 status codes
|
||||||
|
latest_comic_num = response.json()["num"]
|
||||||
|
# Choose a random comic number
|
||||||
|
random_comic_num = random.randint(1, latest_comic_num)
|
||||||
|
# Fetch the random comic data
|
||||||
|
random_comic_url = f"https://xkcd.com/{random_comic_num}/info.0.json"
|
||||||
|
comic_response = requests.get(random_comic_url, timeout=10)
|
||||||
|
comic_response.raise_for_status()
|
||||||
|
comic_data = comic_response.json()
|
||||||
|
image_url = comic_data["img"]
|
||||||
|
# Download the image
|
||||||
|
image_response = requests.get(image_url, timeout=10)
|
||||||
|
image_response.raise_for_status()
|
||||||
|
|
||||||
|
# Use secure temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
|
||||||
|
image_path = temp_file.name
|
||||||
|
temp_file.write(image_response.content)
|
||||||
|
|
||||||
|
# Send the image to the room
|
||||||
|
await bot.api.send_image_message(room_id=room.room_id, image_filepath=image_path)
|
||||||
|
|
||||||
|
# Clean up temp file
|
||||||
|
import os
|
||||||
|
os.remove(image_path)
|
||||||
|
except Exception as e:
|
||||||
|
await bot.api.send_text_message(room.room_id, f"Error fetching XKCD comic: {str(e)}")
|
@@ -1,45 +1,174 @@
|
|||||||
"""
|
"""
|
||||||
This plugin provides a command to fetch YouTube video information from links.
|
Plugin for providing a command to fetch YouTube video information from links.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# plugins/youtube.py
|
# Importing necessary libraries
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
from pytubefix import YouTube
|
|
||||||
import simplematrixbotlib as botlib
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import yt_dlp
|
||||||
|
import simplematrixbotlib as botlib
|
||||||
|
from youtube_title_parse import get_artist_title
|
||||||
|
|
||||||
|
LYRICIST_API_URL = "https://lyrist.vercel.app/api/{}/{}"
|
||||||
|
|
||||||
|
|
||||||
def seconds_to_minutes_seconds(seconds):
|
def seconds_to_minutes_seconds(seconds):
|
||||||
|
"""
|
||||||
|
Converts seconds to a string representation of minutes and seconds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seconds (int): The number of seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A string representation of minutes and seconds in the format MM:SS.
|
||||||
|
"""
|
||||||
minutes = seconds // 60
|
minutes = seconds // 60
|
||||||
seconds %= 60
|
seconds %= 60
|
||||||
return f"{minutes:02d}:{seconds:02d}"
|
return f"{minutes:02d}:{seconds:02d}"
|
||||||
|
|
||||||
async def fetch_youtube_info(youtube_url):
|
|
||||||
|
async def fetch_lyrics(song, artist):
|
||||||
|
"""
|
||||||
|
Asynchronously fetches lyrics for a song from the Lyricist API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
song (str): The name of the song.
|
||||||
|
artist (str): The name of the artist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Lyrics of the song.
|
||||||
|
None if an error occurs during fetching.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
video = YouTube(youtube_url)
|
async with aiohttp.ClientSession() as session:
|
||||||
title = video.title
|
url = LYRICIST_API_URL.format(artist, song)
|
||||||
description = video.description
|
logging.info(f"Fetching lyrics from: {url}")
|
||||||
length = seconds_to_minutes_seconds(video.length)
|
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
||||||
views = video.views
|
if response.status == 200:
|
||||||
author = video.author
|
data = await response.json()
|
||||||
description_with_breaks = description.replace('\n', '<br>')
|
return data.get("lyrics")
|
||||||
info_message = f"""<strong>🎬🎝 Title:</strong> {title} | <strong>Length</strong>: {length} minutes | <strong>Views</strong>: {views}\n<details><summary><strong>⤵︎Click Here For Description⤵︎</strong></summary>{description_with_breaks}</details>"""
|
else:
|
||||||
return info_message
|
logging.warning(f"Lyrics API returned status {response.status}")
|
||||||
|
return None
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logging.error("Timeout fetching lyrics")
|
||||||
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error fetching YouTube video information: {str(e)}")
|
logging.error(f"Error fetching lyrics: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_youtube_info(youtube_url):
|
||||||
|
"""
|
||||||
|
Asynchronously fetches information about a YouTube video using yt-dlp.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
youtube_url (str): The URL of the YouTube video.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A message containing information about the YouTube video.
|
||||||
|
None if an error occurs during fetching.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logging.info(f"Fetching YouTube info for: {youtube_url}")
|
||||||
|
|
||||||
|
# Configure yt-dlp options
|
||||||
|
ydl_opts = {
|
||||||
|
'quiet': True,
|
||||||
|
'no_warnings': True,
|
||||||
|
'extract_flat': False,
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run yt-dlp in thread pool to avoid blocking
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
def extract_info():
|
||||||
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
return ydl.extract_info(youtube_url, download=False)
|
||||||
|
|
||||||
|
info = await loop.run_in_executor(None, extract_info)
|
||||||
|
|
||||||
|
if not info:
|
||||||
|
logging.error("No info returned from yt-dlp")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extract video information
|
||||||
|
title = info.get('title', 'Unknown Title')
|
||||||
|
description = info.get('description', 'No description available')
|
||||||
|
duration = info.get('duration', 0)
|
||||||
|
view_count = info.get('view_count', 0)
|
||||||
|
uploader = info.get('uploader', 'Unknown')
|
||||||
|
|
||||||
|
logging.info(f"Video title: {title}")
|
||||||
|
|
||||||
|
length = seconds_to_minutes_seconds(duration)
|
||||||
|
|
||||||
|
# Parse artist and song from title
|
||||||
|
artist, song = get_artist_title(title)
|
||||||
|
logging.info(f"Parsed artist: {artist}, song: {song}")
|
||||||
|
|
||||||
|
# Limit description length to avoid huge messages
|
||||||
|
if len(description) > 500:
|
||||||
|
description = description[:500] + "..."
|
||||||
|
|
||||||
|
description_with_breaks = description.replace('\n', '<br>')
|
||||||
|
|
||||||
|
# Build basic info message
|
||||||
|
info_message = f"""<strong>🎬🎝 Title:</strong> {title}<br><strong>Length:</strong> {length} | <strong>Views:</strong> {view_count:,} | <strong>Uploader:</strong> {uploader}<br><details><summary><strong>⤵︎Description⤵︎</strong></summary>{description_with_breaks}</details>"""
|
||||||
|
|
||||||
|
# Try to fetch lyrics if artist and song were parsed
|
||||||
|
if artist and song:
|
||||||
|
logging.info("Attempting to fetch lyrics...")
|
||||||
|
lyrics = await fetch_lyrics(song, artist)
|
||||||
|
if lyrics:
|
||||||
|
lyrics = lyrics.replace('\n', "<br>")
|
||||||
|
# Limit lyrics length
|
||||||
|
if len(lyrics) > 3000:
|
||||||
|
lyrics = lyrics[:3000] + "<br>...(truncated)"
|
||||||
|
info_message += f"<br><details><summary><strong>🎵 Lyrics:</strong></summary><br>{lyrics}</details>"
|
||||||
|
else:
|
||||||
|
logging.info("No lyrics found")
|
||||||
|
else:
|
||||||
|
logging.info("Could not parse artist/song from title, skipping lyrics")
|
||||||
|
|
||||||
|
return info_message
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error fetching YouTube video information: {str(e)}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def handle_command(room, message, bot, prefix, config):
|
async def handle_command(room, message, bot, prefix, config):
|
||||||
|
"""
|
||||||
|
Asynchronously handles the command to fetch YouTube video information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (MatrixBot): The Matrix bot instance.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): The bot's configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
match = botlib.MessageMatch(room, message, bot, prefix)
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
if match.is_not_from_this_bot() and re.search(r'youtube\.com/watch\?v=', message.body):
|
|
||||||
logging.info("YouTube link detected")
|
# Check if message contains a YouTube link
|
||||||
video_id_match = re.search(r'youtube\.com/watch\?v=([^\s]+)', message.body)
|
if match.is_not_from_this_bot() and re.search(r'(youtube\.com/watch\?v=|youtu\.be/)', message.body):
|
||||||
|
logging.info(f"YouTube link detected in message: {message.body}")
|
||||||
|
|
||||||
|
# Match both youtube.com and youtu.be formats
|
||||||
|
video_id_match = re.search(r'(?:youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})', message.body)
|
||||||
|
|
||||||
if video_id_match:
|
if video_id_match:
|
||||||
video_id = video_id_match.group(1)
|
video_id = video_id_match.group(1)
|
||||||
youtube_url = f"https://www.youtube.com/watch?v={video_id}"
|
youtube_url = f"https://www.youtube.com/watch?v={video_id}"
|
||||||
logging.info(f"Fetching information for YouTube video: {youtube_url}")
|
logging.info(f"Fetching information for YouTube video ID: {video_id}")
|
||||||
retry_count = 3
|
|
||||||
|
retry_count = 2 # Reduced retries since yt-dlp is more reliable
|
||||||
while retry_count > 0:
|
while retry_count > 0:
|
||||||
info_message = await fetch_youtube_info(youtube_url)
|
info_message = await fetch_youtube_info(youtube_url)
|
||||||
if info_message:
|
if info_message:
|
||||||
@@ -47,10 +176,12 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
logging.info("Sent YouTube video information to the room")
|
logging.info("Sent YouTube video information to the room")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
logging.info("Retrying...")
|
logging.warning(f"Failed to fetch info, retrying... ({retry_count-1} attempts left)")
|
||||||
retry_count -= 1
|
retry_count -= 1
|
||||||
await asyncio.sleep(1) # wait for 1 second before retrying
|
if retry_count > 0:
|
||||||
|
await asyncio.sleep(2) # wait for 2 seconds before retrying
|
||||||
else:
|
else:
|
||||||
logging.error("Failed to fetch YouTube video information after retries")
|
logging.error("Failed to fetch YouTube video information after all retries")
|
||||||
|
await bot.api.send_text_message(room.room_id, "Failed to fetch YouTube video information. The video may be unavailable or age-restricted.")
|
||||||
|
else:
|
||||||
|
logging.warning("Could not extract video ID from YouTube URL")
|
||||||
|
@@ -1,13 +1,25 @@
|
|||||||
"""
|
"""
|
||||||
This plugin provides a command to search for YouTube videos in the room
|
Plugin for providing a command to search for YouTube videos in the room.
|
||||||
"""
|
"""
|
||||||
# plugins/youtube_search.py
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import simplematrixbotlib as botlib
|
import simplematrixbotlib as botlib
|
||||||
from youtube_search import YoutubeSearch
|
from youtube_search import YoutubeSearch
|
||||||
|
|
||||||
async def handle_command(room, message, bot, PREFIX, config):
|
async def handle_command(room, message, bot, PREFIX, config):
|
||||||
|
"""
|
||||||
|
Asynchronously handles the command to search for YouTube videos in the room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the command was invoked.
|
||||||
|
message (RoomMessage): The message object containing the command.
|
||||||
|
bot (MatrixBot): The Matrix bot instance.
|
||||||
|
PREFIX (str): The command prefix.
|
||||||
|
config (dict): The bot's configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
match = botlib.MessageMatch(room, message, bot, PREFIX)
|
match = botlib.MessageMatch(room, message, bot, PREFIX)
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("yt"):
|
if match.is_not_from_this_bot() and match.prefix() and match.command("yt"):
|
||||||
args = match.args()
|
args = match.args()
|
||||||
@@ -16,7 +28,7 @@ async def handle_command(room, message, bot, PREFIX, config):
|
|||||||
else:
|
else:
|
||||||
search_terms = " ".join(args)
|
search_terms = " ".join(args)
|
||||||
logging.info(f"Performing YouTube search for: {search_terms}")
|
logging.info(f"Performing YouTube search for: {search_terms}")
|
||||||
results = YoutubeSearch(search_terms, max_results=3).to_dict()
|
results = YoutubeSearch(search_terms, max_results=1).to_dict()
|
||||||
if results:
|
if results:
|
||||||
output = generate_output(results)
|
output = generate_output(results)
|
||||||
await send_collapsible_message(room, bot, output)
|
await send_collapsible_message(room, bot, output)
|
||||||
@@ -24,6 +36,15 @@ async def handle_command(room, message, bot, PREFIX, config):
|
|||||||
await bot.api.send_text_message(room.room_id, "No results found.")
|
await bot.api.send_text_message(room.room_id, "No results found.")
|
||||||
|
|
||||||
def generate_output(results):
|
def generate_output(results):
|
||||||
|
"""
|
||||||
|
Generates HTML output for displaying YouTube search results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results (list): A list of dictionaries containing information about YouTube videos.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: HTML formatted output containing YouTube search results.
|
||||||
|
"""
|
||||||
output = ""
|
output = ""
|
||||||
for video in results:
|
for video in results:
|
||||||
output += f'<a href="https://www.youtube.com/watch?v={video["id"]}">'
|
output += f'<a href="https://www.youtube.com/watch?v={video["id"]}">'
|
||||||
@@ -37,5 +58,16 @@ def generate_output(results):
|
|||||||
|
|
||||||
|
|
||||||
async def send_collapsible_message(room, bot, content):
|
async def send_collapsible_message(room, bot, content):
|
||||||
|
"""
|
||||||
|
Sends a collapsible message containing YouTube search results to the room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room (Room): The Matrix room where the message will be sent.
|
||||||
|
bot (MatrixBot): The Matrix bot instance.
|
||||||
|
content (str): HTML content to be included in the collapsible message.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
message = f'<details><summary><strong>🍄Funguy ▶YouTube Search🍄<br>⤵︎Click Here To See Results⤵︎</strong></summary>{content}</details>'
|
message = f'<details><summary><strong>🍄Funguy ▶YouTube Search🍄<br>⤵︎Click Here To See Results⤵︎</strong></summary>{content}</details>'
|
||||||
await bot.api.send_markdown_message(room.room_id, message)
|
await bot.api.send_markdown_message(room.room_id, message)
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
requests
|
requests
|
||||||
pytubefix
|
|
||||||
duckduckgo_search
|
duckduckgo_search
|
||||||
nio
|
nio
|
||||||
markdown2
|
markdown2
|
||||||
watchdog
|
watchdog
|
||||||
emoji
|
emoji
|
||||||
|
python-slugify
|
||||||
|
youtube_title_parse
|
||||||
|
dnspython
|
||||||
|
croniter
|
||||||
|
schedule
|
||||||
|
yt-dlp
|
||||||
|
pyopenssl
|
||||||
|
Reference in New Issue
Block a user