323 lines
8.9 KiB
Bash
Executable file
323 lines
8.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# learner-add.sh - Add a new learner account for maubot plugin development
|
|
# Usage: learner-add.sh <username> <ssh-pubkey>
|
|
#
|
|
# Creates:
|
|
# - Unix user account with SSH key
|
|
# - ~/plugins directory with hello-world starter plugin
|
|
# - Symlink in maubot plugins directory
|
|
# - Outputs onboarding instructions
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
MAUBOT_PLUGINS_DIR="/var/lib/maubot/plugins"
|
|
TEMPLATE_DIR="${SCRIPT_DIR}/../templates/plugin-skeleton"
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
|
|
|
usage() {
|
|
echo "Usage: $0 <username> <ssh-pubkey>"
|
|
echo ""
|
|
echo "Arguments:"
|
|
echo " username - Learner's username (alphanumeric, 3-16 chars)"
|
|
echo " ssh-pubkey - SSH public key (starts with ssh-ed25519, ssh-rsa, etc.)"
|
|
echo ""
|
|
echo "Example:"
|
|
echo " $0 alice 'ssh-ed25519 AAAA... alice@laptop'"
|
|
exit 1
|
|
}
|
|
|
|
validate_username() {
|
|
local username="$1"
|
|
if [[ ! "$username" =~ ^[a-z][a-z0-9_-]{2,15}$ ]]; then
|
|
log_error "Invalid username: must be 3-16 chars, start with letter, alphanumeric/underscore/dash only"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if user already exists
|
|
if id "$username" &>/dev/null; then
|
|
log_error "User '$username' already exists"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_ssh_key() {
|
|
local key="$1"
|
|
if [[ ! "$key" =~ ^ssh-(ed25519|rsa|ecdsa) ]]; then
|
|
log_error "Invalid SSH key: must start with ssh-ed25519, ssh-rsa, or ssh-ecdsa"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
create_user() {
|
|
local username="$1"
|
|
local ssh_key="$2"
|
|
|
|
log_info "Creating user '$username'..."
|
|
|
|
# Create user with home directory
|
|
# NixOS: don't specify shell (uses default), group is 'users'
|
|
useradd -m -g users "$username"
|
|
|
|
# Add to learners group for Slack token access
|
|
usermod -aG learners "$username"
|
|
|
|
# Set up SSH key
|
|
local ssh_dir="/home/$username/.ssh"
|
|
mkdir -p "$ssh_dir"
|
|
echo "$ssh_key" > "$ssh_dir/authorized_keys"
|
|
chmod 700 "$ssh_dir"
|
|
chmod 600 "$ssh_dir/authorized_keys"
|
|
chown -R "$username:users" "$ssh_dir"
|
|
|
|
# Add Slack env vars to bashrc
|
|
echo '' >> "/home/$username/.bashrc"
|
|
echo '# Slack bot development tokens' >> "/home/$username/.bashrc"
|
|
echo 'source /etc/slack-learner.env' >> "/home/$username/.bashrc"
|
|
|
|
# Set up ~/.local/bin with claude symlink
|
|
local local_bin="/home/$username/.local/bin"
|
|
mkdir -p "$local_bin"
|
|
ln -sf /usr/local/bin/claude "$local_bin/claude"
|
|
chown -R "$username:users" "/home/$username/.local"
|
|
|
|
log_info "User created with SSH access"
|
|
}
|
|
|
|
setup_plugin_directory() {
|
|
local username="$1"
|
|
local plugin_name="hello-${username}"
|
|
local user_plugins_dir="/home/$username/plugins"
|
|
local plugin_dir="$user_plugins_dir/$plugin_name"
|
|
|
|
log_info "Setting up plugin directory..."
|
|
|
|
# Create plugins directory
|
|
mkdir -p "$plugin_dir"
|
|
mkdir -p "$plugin_dir/dist"
|
|
|
|
# Python module names must use underscores, not hyphens
|
|
local module_name="${plugin_name//-/_}"
|
|
|
|
# Copy template if exists, otherwise create from scratch
|
|
if [[ -d "$TEMPLATE_DIR" ]]; then
|
|
cp -r "$TEMPLATE_DIR/"* "$plugin_dir/"
|
|
# Rename MODULE_NAME directory to actual module name
|
|
if [[ -d "$plugin_dir/MODULE_NAME" ]]; then
|
|
mv "$plugin_dir/MODULE_NAME" "$plugin_dir/$module_name"
|
|
fi
|
|
# Replace placeholders in all files
|
|
find "$plugin_dir" -type f -exec sed -i "s/MODULE_NAME/$module_name/g" {} \;
|
|
find "$plugin_dir" -type f -exec sed -i "s/USERNAME/$username/g" {} \;
|
|
else
|
|
# Create minimal starter plugin
|
|
create_starter_plugin "$plugin_dir" "$module_name" "$username"
|
|
fi
|
|
|
|
# Set ownership
|
|
chown -R "$username:users" "$user_plugins_dir"
|
|
|
|
# Make plugin dir readable by maubot group
|
|
chmod 755 "/home/$username"
|
|
chmod 755 "$user_plugins_dir"
|
|
chmod -R 755 "$plugin_dir"
|
|
|
|
log_info "Plugin directory created at $plugin_dir"
|
|
}
|
|
|
|
create_starter_plugin() {
|
|
local plugin_dir="$1"
|
|
local module_name="$2"
|
|
local username="$3"
|
|
|
|
# Create module directory
|
|
mkdir -p "$plugin_dir/$module_name"
|
|
|
|
# maubot.yaml
|
|
cat > "$plugin_dir/maubot.yaml" << EOF
|
|
maubot: 0.4.0
|
|
id: xyz.clarun.${module_name}
|
|
version: 0.1.0
|
|
license: MIT
|
|
modules:
|
|
- ${module_name}
|
|
main_class: ${module_name}.Bot
|
|
EOF
|
|
|
|
# Python module __init__.py
|
|
cat > "$plugin_dir/$module_name/__init__.py" << EOF
|
|
from .bot import Bot
|
|
|
|
__all__ = ["Bot"]
|
|
EOF
|
|
|
|
# Main bot.py
|
|
cat > "$plugin_dir/$module_name/bot.py" << EOF
|
|
from maubot import Plugin, MessageEvent
|
|
from maubot.handlers import command
|
|
|
|
|
|
class Bot(Plugin):
|
|
"""Hello world bot for $username"""
|
|
|
|
@command.new("hello")
|
|
async def hello_command(self, evt: MessageEvent) -> None:
|
|
"""Respond to !hello command"""
|
|
await evt.reply(f"Hello from ${username}'s bot!")
|
|
|
|
@command.new("ping")
|
|
async def ping_command(self, evt: MessageEvent) -> None:
|
|
"""Respond to !ping command"""
|
|
await evt.reply("Pong!")
|
|
EOF
|
|
|
|
# Makefile
|
|
cat > "$plugin_dir/Makefile" << 'EOF'
|
|
PLUGIN_NAME := $(shell grep '^id:' maubot.yaml | cut -d: -f2 | tr -d ' ')
|
|
PLUGIN_VERSION := $(shell grep '^version:' maubot.yaml | cut -d: -f2 | tr -d ' ')
|
|
MBP_FILE := dist/$(PLUGIN_NAME)-$(PLUGIN_VERSION).mbp
|
|
|
|
.PHONY: build reload clean help
|
|
|
|
help:
|
|
@echo "Available targets:"
|
|
@echo " build - Build the .mbp plugin file"
|
|
@echo " reload - Reload plugin in maubot (requires instance configured)"
|
|
@echo " clean - Remove built files"
|
|
@echo " dev - Build and reload"
|
|
|
|
build:
|
|
@echo "Building $(PLUGIN_NAME)..."
|
|
@mkdir -p dist
|
|
@rm -f $(MBP_FILE)
|
|
@zip -r $(MBP_FILE) maubot.yaml $(shell grep -E '^ - ' maubot.yaml | sed 's/ - //')
|
|
@echo "Built: $(MBP_FILE)"
|
|
|
|
reload:
|
|
@echo "Reloading plugin..."
|
|
@echo "TODO: Add maubot API reload command"
|
|
|
|
clean:
|
|
rm -rf dist/*.mbp
|
|
|
|
dev: build reload
|
|
EOF
|
|
|
|
# README
|
|
cat > "$plugin_dir/README.md" << EOF
|
|
# $module_name
|
|
|
|
Hello world maubot plugin for $username.
|
|
|
|
## Quick Start
|
|
|
|
1. Edit \`$module_name/bot.py\` to add your bot logic
|
|
2. Run \`make build\` to create the .mbp file
|
|
3. Upload to maubot via admin UI (http://localhost:29316)
|
|
|
|
## Commands
|
|
|
|
- \`!hello\` - Says hello
|
|
- \`!ping\` - Responds with pong
|
|
|
|
## Files
|
|
|
|
- \`maubot.yaml\` - Plugin manifest
|
|
- \`$module_name/bot.py\` - Main bot code
|
|
- \`Makefile\` - Build commands
|
|
EOF
|
|
}
|
|
|
|
create_maubot_symlink() {
|
|
local username="$1"
|
|
local plugin_name="hello-${username}"
|
|
local plugin_dir="/home/$username/plugins/$plugin_name"
|
|
local symlink_path="$MAUBOT_PLUGINS_DIR/${username}-${plugin_name}.mbp"
|
|
|
|
log_info "Creating maubot plugin symlink..."
|
|
|
|
# Note: Symlink will point to the built .mbp file
|
|
# For now, we'll link to the dist directory's expected output
|
|
# The learner will need to run 'make build' first
|
|
|
|
# We could also configure maubot to load from source directories
|
|
# but .mbp symlinks are simpler for now
|
|
|
|
log_warn "Learner must run 'make build' before plugin appears in maubot"
|
|
}
|
|
|
|
print_onboarding() {
|
|
local username="$1"
|
|
local server_ip
|
|
# NixOS: use ip command instead of hostname -I
|
|
server_ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " Dev Environment Ready: $username"
|
|
echo "=========================================="
|
|
echo ""
|
|
echo "## SSH Config (~/.ssh/config on your laptop)"
|
|
echo ""
|
|
echo " Host dev-server"
|
|
echo " HostName ${server_ip:-<server-ip>}"
|
|
echo " User $username"
|
|
echo " LocalForward 8080 127.0.0.1:8080"
|
|
echo ""
|
|
echo "## Quick Start"
|
|
echo ""
|
|
echo "1. SSH in:"
|
|
echo " ssh dev-server"
|
|
echo ""
|
|
echo "2. Authenticate Claude (first time only):"
|
|
echo " claude"
|
|
echo " # Opens localhost URL - paste in your local browser"
|
|
echo " # Complete OAuth, token flows back automatically"
|
|
echo ""
|
|
echo "3. Start coding:"
|
|
echo " mkdir mybot && cd mybot"
|
|
echo " claude 'create a slack bot that responds to hello'"
|
|
echo ""
|
|
echo "## Tools Available"
|
|
echo " python3, uv, go (nix profile install nixpkgs#go)"
|
|
echo " Slack tokens: \$SLACK_BOT_TOKEN, \$SLACK_APP_TOKEN"
|
|
echo ""
|
|
echo "=========================================="
|
|
}
|
|
|
|
main() {
|
|
if [[ $# -lt 2 ]]; then
|
|
usage
|
|
fi
|
|
|
|
local username="$1"
|
|
local ssh_key="$2"
|
|
|
|
# Must run as root
|
|
if [[ $EUID -ne 0 ]]; then
|
|
log_error "This script must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
validate_username "$username"
|
|
validate_ssh_key "$ssh_key"
|
|
|
|
create_user "$username" "$ssh_key"
|
|
setup_plugin_directory "$username"
|
|
create_maubot_symlink "$username"
|
|
print_onboarding "$username"
|
|
|
|
log_info "Learner '$username' setup complete!"
|
|
}
|
|
|
|
main "$@"
|