Distributing Modules
You wrote a module. It's beautiful. It does something useful (or useless, but fun). And now you want to share it with the world.
There are three ways to distribute your modules.
Method 1: Repository (RECOMMENDED)
This is the civilized way. Like pip, npm, but for MXUserBot.
How It Works
You upload modules to GitHub/GitLab/any git host, generate index.json and tell users to add your repository. That's it.
Step by Step
-
Create the structure:
your-repo/ ← your git repo name ├── modules/ ← modules FOLDER (always named this way) │ ├── wikipedia.py ← single file module │ ├── calculator.py ← another single file │ └── myepicmodule/ ← FOLDER = ZIP module [multiple files] │ ├── __init__.py │ └── utils.py └── index.json ← generated by the script below -
Change
BASE_URLin the script to your own:Format:
Example: if your repo is
https://github.com/miku1337/my-modules, then: -
Run the script — it will create index.json and zip folder-based modules
-
Push everything to GitHub
-
Tell users to add:
Script to Generate index.json
Click to see the code (spoiler)
import ast
import io
import json
import zipfile
from pathlib import Path
BASE_URL = "https://raw.githubusercontent.com/YOUR_USERNAME/REPO_NAME/main/modules" # ← CHANGE TO YOURS!
SKIP_DIRS = {"__pycache__"}
SKIP_EXTS = {".pyc", ".pyo"}
SKIP_FILES = {"__pycache__"}
def extract_value(node):
try:
return ast.literal_eval(node)
except Exception:
return None
def extract_meta(source: str, default_url: str) -> dict | None:
try:
tree = ast.parse(source)
except (SyntaxError, UnicodeDecodeError):
return None
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.name == "Meta":
meta = {
"url": default_url,
"author": "Unknown",
"dependencies": [],
"tags": [],
}
mapping = {
"name": "name",
"description": "description",
"version": "version",
"dependencies": "dependencies",
"tags": "tags",
"author": "author",
}
for item in node.body:
if isinstance(item, ast.Assign):
for target in item.targets:
if isinstance(target, ast.Name) and target.id in mapping:
val = extract_value(item.value)
if val is not None:
meta[mapping[target.id]] = val
return meta
return None
def package_folder(folder: Path) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
for path in folder.rglob("*"):
if path.is_dir():
continue
rel = path.relative_to(folder.parent)
if path.name in SKIP_FILES or path.suffix in SKIP_EXTS:
continue
if any(part in SKIP_DIRS for part in path.relative_to(folder).parts):
continue
zf.write(path, str(rel))
return buf.getvalue()
def generate_index():
folder_name = BASE_URL.rstrip("/").split("/")[-1]
modules_dir = Path(folder_name)
index_data = {}
if not modules_dir.exists() or not modules_dir.is_dir():
print(f"Folder {modules_dir} not found")
return
for file in sorted(modules_dir.glob("*.py")):
if file.name.startswith("_"):
continue
meta = extract_meta(file.read_text("utf-8"), f"{BASE_URL.rstrip('/')}/{file.name}")
if meta:
index_data[file.stem] = meta
print(f"Indexed: {file.stem}")
else:
print(f"Skipped (no Meta): {file.name}")
for folder in sorted(modules_dir.iterdir()):
if not folder.is_dir() or folder.name.startswith("_") or folder.name in SKIP_DIRS:
continue
init_file = folder / "__init__.py"
if not init_file.exists():
print(f"Skipped folder (no __init__.py): {folder.name}/")
continue
zip_name = f"{folder.name}.zip"
zip_path = modules_dir / zip_name
source = init_file.read_text("utf-8")
meta = extract_meta(source, f"{BASE_URL.rstrip('/')}/{zip_name}")
if not meta:
print(f"Skipped folder (no Meta): {folder.name}/")
continue
data = package_folder(folder)
zip_path.write_bytes(data)
index_data[folder.name] = meta
print(f"Packed & indexed: {folder.name}/ → {zip_name}")
output_file = Path("index.json")
output_file.write_text(
json.dumps(index_data, indent=2, ensure_ascii=False),
encoding="utf-8",
)
print(f"\nindex.json written ({len(index_data)} entries)")
if __name__ == "__main__":
generate_index()
How Users Will Install Your Modules
After .addrepo <url> modules are available as:
For example:
Web Interface
Easier through the web: modules → store → search.
About "community" repos
By default, your repository is marked as community — an untrusted source. The user will see a warning on install. Nothing personal, just security.
Want to be in the official repo?
If you want your module in the main MXUserBot repository:
- Clone mx-modules
- Add your module to
modules/ - Run
script.pyto updateindex.json - Submit a PR (Pull Request)
Method 2: Share a File
For quick development or if you're lazy.
How It Works:
- You drop a
wikipedia.pyfile in the room - The user replies to it with
.mdl dev - The bot shows a warning: "Untrusted source, are you sure?"
- User confirms — module installed
Perfect for testing or when you can't be bothered with a repository.
Method 3: Direct Link
Same as method 2, but instead of a file — a direct raw link:
The user will get the same untrusted source warning.
Quick Cheat Sheet
| Method | When to use | Trust level |
|---|---|---|
| Repository | Publishing for others | community (warning) |
| File | Development, testing | dev (warning) |
| Link | Quick share | dev (warning) |
Tip: Use a repository. It's clean, civilized, and lets users update modules via .mdl update.