Compare commits
26 Commits
c4b6c750fb
...
main
Author | SHA1 | Date | |
---|---|---|---|
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
|
||||||
|
294
README.md
294
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,279 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
### 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",]
|
||||||
|
254
funguy.py
254
funguy.py
@@ -1,95 +1,255 @@
|
|||||||
#!/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'
|
||||||
|
}
|
||||||
|
|
||||||
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]
|
"""
|
||||||
|
# Iterating through whitelisted plugins only
|
||||||
|
for plugin_name in ALLOWED_PLUGINS:
|
||||||
|
plugin_file = os.path.join(self.PLUGINS_DIR, f"{plugin_name}.py")
|
||||||
|
|
||||||
|
# Verify that the plugin file exists
|
||||||
|
if not os.path.isfile(plugin_file):
|
||||||
|
logging.warning(f"Plugin file not found: {plugin_file}, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Importing plugin module dynamically with validated plugin name
|
||||||
module = importlib.import_module(f"{self.PLUGINS_DIR}.{plugin_name}")
|
module = importlib.import_module(f"{self.PLUGINS_DIR}.{plugin_name}")
|
||||||
self.PLUGINS[plugin_name] = module
|
self.PLUGINS[plugin_name] = module # Storing loaded plugin module
|
||||||
logging.info(f"Loaded plugin: {plugin_name}")
|
logging.info(f"Loaded plugin: {plugin_name}") # Logging successful plugin loading
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading plugin {plugin_name}: {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():
|
||||||
|
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)
|
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)
|
198
plugins/help.py
198
plugins/help.py
@@ -1,9 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
@@ -14,6 +12,9 @@ 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 (MatrixBot): The Matrix bot instance.
|
||||||
|
prefix (str): The command prefix.
|
||||||
|
config (dict): The bot's configuration.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
@@ -22,48 +23,211 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
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>!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>
|
||||||
"""
|
"""
|
||||||
|
@@ -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,35 +1,139 @@
|
|||||||
"""
|
"""
|
||||||
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 unload_plugin(plugin_name):
|
||||||
|
"""
|
||||||
|
Asynchronously unloads a plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_name (str): The name of the plugin to unload.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the plugin is unloaded successfully, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if plugin_name in PLUGINS:
|
||||||
|
del PLUGINS[plugin_name] # Remove the plugin from the PLUGINS dictionary
|
||||||
|
del sys.modules[f"plugins.{plugin_name}"] # Unload the plugin module from sys.modules
|
||||||
|
logging.info(f"Unloaded plugin: {plugin_name}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
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):
|
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)
|
match = botlib.MessageMatch(room, message, bot, prefix)
|
||||||
if match.is_not_from_this_bot() and match.prefix() and match.command("load"):
|
if match.is_not_from_this_bot() and match.prefix():
|
||||||
|
command = match.command()
|
||||||
|
if command == "load":
|
||||||
if str(message.sender) == config.admin_user:
|
if str(message.sender) == config.admin_user:
|
||||||
args = match.args()
|
args = match.args()
|
||||||
if len(args) != 1:
|
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>")
|
await bot.api.send_text_message(room.room_id, "Usage: !load <plugin>")
|
||||||
else:
|
else:
|
||||||
plugin_name = args[0]
|
plugin_name = args[0]
|
||||||
|
# Check if the plugin is not already loaded
|
||||||
if plugin_name not in PLUGINS:
|
if plugin_name not in PLUGINS:
|
||||||
|
# Load the plugin
|
||||||
success = await load_plugin(plugin_name)
|
success = await load_plugin(plugin_name)
|
||||||
if success:
|
if success:
|
||||||
await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' loaded successfully")
|
await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' loaded successfully")
|
||||||
@@ -38,4 +142,22 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
else:
|
else:
|
||||||
await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' is already loaded")
|
await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' is already loaded")
|
||||||
else:
|
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.")
|
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.")
|
||||||
|
139
plugins/proxy.py
139
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,7 +156,16 @@ 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")
|
||||||
|
|
||||||
|
# Check database for a working proxy
|
||||||
|
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}")
|
||||||
|
return
|
||||||
|
|
||||||
# Download proxy list if needed
|
# Download proxy list if needed
|
||||||
|
else:
|
||||||
if not await download_proxy_list():
|
if not await download_proxy_list():
|
||||||
await bot.api.send_markdown_message(room.room_id, "Error downloading proxy list")
|
await bot.api.send_markdown_message(room.room_id, "Error downloading proxy list")
|
||||||
logging.error("Error downloading proxy list")
|
logging.error("Error downloading proxy list")
|
||||||
@@ -96,25 +174,40 @@ async def handle_command(room, message, bot, prefix, config):
|
|||||||
try:
|
try:
|
||||||
# Read proxies from file
|
# Read proxies from file
|
||||||
with open(PROXY_LIST_FILENAME, 'r') as f:
|
with open(PROXY_LIST_FILENAME, 'r') as f:
|
||||||
socks5_proxies = f.read().splitlines()
|
socks5_proxies = [line.replace("socks5://", "") for line in f.read().splitlines()]
|
||||||
random.shuffle(socks5_proxies)
|
random.shuffle(socks5_proxies)
|
||||||
|
|
||||||
# Test proxies
|
# Test proxies concurrently
|
||||||
socks5_proxy = None
|
tested_proxies = 0
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
|
||||||
|
futures = []
|
||||||
for proxy in socks5_proxies[:MAX_TRIES]:
|
for proxy in socks5_proxies[:MAX_TRIES]:
|
||||||
success, latency = await test_socks5_proxy(proxy)
|
futures.append(executor.submit(test_proxy, proxy))
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
success, proxy, latency = future.result()
|
||||||
if success:
|
if success:
|
||||||
socks5_proxy = proxy
|
await bot.api.send_markdown_message(room.room_id,
|
||||||
break
|
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}")
|
||||||
|
|
||||||
# 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:
|
else:
|
||||||
|
# If no working proxy found after testing
|
||||||
await bot.api.send_markdown_message(room.room_id, "❌ No working anonymous SOCKS5 proxy found")
|
await bot.api.send_markdown_message(room.room_id, "❌ No working anonymous SOCKS5 proxy found")
|
||||||
logging.info("No working anonymous SOCKS5 proxy found")
|
logging.info("No working anonymous SOCKS5 proxy found")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error handling !proxy command: {e}")
|
logging.error(f"Error handling !proxy command: {e}")
|
||||||
await bot.api.send_markdown_message(room.room_id, "❌ Error handling !proxy command")
|
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}")
|
@@ -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")):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if API is available
|
||||||
try:
|
try:
|
||||||
parser = argparse.ArgumentParser(description='Generate images using self hosted Stable Diffusion')
|
health_check = requests.get("http://127.0.0.1:7860/docs", timeout=3)
|
||||||
parser.add_argument('--steps', type=int, default=4, help='Number of steps, default=16')
|
if health_check.status_code != 200:
|
||||||
parser.add_argument('--cfg', type=int, default=1.25, help='CFG scale, default=7')
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 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('--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('--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('--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', 'Karras'], help='Sampler name, default=Euler a')
|
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')
|
parser.add_argument('prompt', type=str, nargs='*', help='Prompt for the image')
|
||||||
|
|
||||||
args = parser.parse_args(message.body.split()[1:]) # Skip the command itself
|
args = parser.parse_args(message.body.split()[1:]) # skip command prefix
|
||||||
|
|
||||||
if not args.prompt:
|
if not args.prompt:
|
||||||
raise argparse.ArgumentError(None, "Prompt is required.")
|
raise argparse.ArgumentError(None, "Prompt is required.")
|
||||||
|
|
||||||
prompt = ' '.join(args.prompt)
|
prompt = ' '.join(args.prompt)
|
||||||
sampler_name = ' '.join(args.sampler)
|
sampler_name = ' '.join(args.sampler)
|
||||||
neg = ' '.join(args.neg)
|
neg_prompt = ' '.join(args.neg)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"steps": args.steps,
|
"steps": args.steps,
|
||||||
"negative_prompt": neg,
|
"negative_prompt": neg_prompt,
|
||||||
"sampler_name": sampler_name,
|
"sampler_name": sampler_name,
|
||||||
"cfg_scale": args.cfg,
|
"cfg_scale": args.cfg,
|
||||||
"width": args.w,
|
"width": args.w,
|
||||||
"height": args.h,
|
"height": args.h,
|
||||||
}
|
}
|
||||||
url = "http://127.0.0.1:7860/sdapi/v1/txt2img"
|
|
||||||
|
|
||||||
response = requests.post(url=url, json=payload)
|
url = "http://127.0.0.1:7860/sdapi/v1/txt2img"
|
||||||
|
response = requests.post(url=url, json=payload, timeout=600)
|
||||||
r = response.json()
|
r = response.json()
|
||||||
with open("/tmp/output.jpg", 'wb') as f:
|
|
||||||
f.write(base64.b64decode(r['images'][0]))
|
# Use secure temporary file
|
||||||
await bot.api.send_image_message(room_id=room.room_id, image_filepath="/tmp/output.jpg") # Corrected argument name
|
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:
|
except argparse.ArgumentError as e:
|
||||||
await bot.api.send_text_message(room.room_id, f"Error: {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>")
|
await bot.api.send_markdown_message(room.room_id, "<details><summary>Stable Diffusion Help</summary>" + print_help() + "</details>")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await bot.api.send_text_message(room.room_id, f"Error processing the command: {str(e)}")
|
await bot.api.send_text_message(room.room_id, f"Error processing the command: {str(e)}")
|
||||||
finally:
|
finally:
|
||||||
|
# Process next queued command
|
||||||
if not command_queue.empty():
|
if not command_queue.empty():
|
||||||
next_command = await command_queue.get()
|
next_command = await command_queue.get()
|
||||||
await handle_command(*next_command)
|
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,13 @@
|
|||||||
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
|
||||||
|
Reference in New Issue
Block a user