diff --git a/.env.example b/.env.example index 9ca59fd..f759171 100644 --- a/.env.example +++ b/.env.example @@ -350,6 +350,28 @@ OLLAMA_MODEL=llama3.1:latest # Get your API key from: https://platform.openai.com/api-keys # OPENAI_API_KEY=sk-... +# ====================== +# Matrix Dev Environment (docker-compose.matrix.yml overlay) +# ====================== +# These variables configure the local Matrix dev environment. +# Only used when running: docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml up +# +# Synapse homeserver +# SYNAPSE_CLIENT_PORT=8008 +# SYNAPSE_FEDERATION_PORT=8448 +# SYNAPSE_POSTGRES_DB=synapse +# SYNAPSE_POSTGRES_USER=synapse +# SYNAPSE_POSTGRES_PASSWORD=synapse_dev_password +# +# Element Web client +# ELEMENT_PORT=8501 +# +# Matrix bridge connection (set after running docker/matrix/scripts/setup-bot.sh) +# MATRIX_HOMESERVER_URL=http://localhost:8008 +# MATRIX_ACCESS_TOKEN= +# MATRIX_BOT_USER_ID=@mosaic-bot:localhost +# MATRIX_SERVER_NAME=localhost + # ====================== # Logging & Debugging # ====================== diff --git a/Makefile b/Makefile index 3375fee..ac2718b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install dev build test docker-up docker-down docker-logs docker-ps docker-build docker-restart docker-test clean +.PHONY: help install dev build test docker-up docker-down docker-logs docker-ps docker-build docker-restart docker-test clean matrix-up matrix-down matrix-logs matrix-setup-bot # Default target help: @@ -24,6 +24,12 @@ help: @echo " make docker-test Run Docker smoke test" @echo " make docker-test-traefik Run Traefik integration tests" @echo "" + @echo "Matrix Dev Environment:" + @echo " make matrix-up Start Matrix services (Synapse + Element)" + @echo " make matrix-down Stop Matrix services" + @echo " make matrix-logs View Matrix service logs" + @echo " make matrix-setup-bot Create bot account and get access token" + @echo "" @echo "Database:" @echo " make db-migrate Run database migrations" @echo " make db-seed Seed development data" @@ -85,6 +91,19 @@ docker-test: docker-test-traefik: ./tests/integration/docker/traefik.test.sh all +# Matrix Dev Environment +matrix-up: + docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml up -d + +matrix-down: + docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml down + +matrix-logs: + docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml logs -f synapse element-web + +matrix-setup-bot: + docker/matrix/scripts/setup-bot.sh + # Database operations db-migrate: cd apps/api && pnpm prisma:migrate diff --git a/docker/docker-compose.matrix.yml b/docker/docker-compose.matrix.yml new file mode 100644 index 0000000..b850458 --- /dev/null +++ b/docker/docker-compose.matrix.yml @@ -0,0 +1,129 @@ +# ============================================== +# Matrix Dev Environment (Synapse + Element Web) +# ============================================== +# +# Development-only overlay for testing the Matrix bridge locally. +# NOT for production — use docker-compose.sample.matrix.yml for production. +# +# Usage: +# docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml up -d +# +# Or with Makefile: +# make matrix-up +# +# This overlay: +# - Adds Synapse homeserver (localhost:8008) using shared PostgreSQL +# - Adds Element Web client (localhost:8501) +# - Creates a separate 'synapse' database in the shared PostgreSQL instance +# - Enables open registration for easy dev testing +# +# After first startup, create the bot account: +# docker/matrix/scripts/setup-bot.sh +# +# ============================================== + +services: + # ====================== + # Synapse Database Init + # ====================== + # Creates the 'synapse' database and user in the shared PostgreSQL instance. + # Runs once and exits — idempotent, safe to run repeatedly. + synapse-db-init: + image: postgres:17-alpine + container_name: mosaic-synapse-db-init + restart: "no" + environment: + PGHOST: postgres + PGPORT: 5432 + PGUSER: ${POSTGRES_USER:-mosaic} + PGPASSWORD: ${POSTGRES_PASSWORD:-mosaic_dev_password} + SYNAPSE_DB: ${SYNAPSE_POSTGRES_DB:-synapse} + SYNAPSE_USER: ${SYNAPSE_POSTGRES_USER:-synapse} + SYNAPSE_PASSWORD: ${SYNAPSE_POSTGRES_PASSWORD:-synapse_dev_password} + entrypoint: ["sh", "-c"] + command: + - | + until pg_isready -h postgres -p 5432 -U $${PGUSER}; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + echo "PostgreSQL is ready. Creating Synapse database and user..." + + psql -h postgres -U $${PGUSER} -tc "SELECT 1 FROM pg_roles WHERE rolname='$${SYNAPSE_USER}'" | grep -q 1 || \ + psql -h postgres -U $${PGUSER} -c "CREATE USER $${SYNAPSE_USER} WITH PASSWORD '$${SYNAPSE_PASSWORD}';" + + psql -h postgres -U $${PGUSER} -tc "SELECT 1 FROM pg_database WHERE datname='$${SYNAPSE_DB}'" | grep -q 1 || \ + psql -h postgres -U $${PGUSER} -c "CREATE DATABASE $${SYNAPSE_DB} OWNER $${SYNAPSE_USER} ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE template0;" + + echo "Synapse database ready: $${SYNAPSE_DB}" + depends_on: + postgres: + condition: service_healthy + networks: + - mosaic-network + + # ====================== + # Synapse (Matrix Homeserver) + # ====================== + synapse: + image: matrixdotorg/synapse:latest + container_name: mosaic-synapse + restart: unless-stopped + environment: + SYNAPSE_CONFIG_DIR: /data + SYNAPSE_CONFIG_PATH: /data/homeserver.yaml + ports: + - "${SYNAPSE_CLIENT_PORT:-8008}:8008" + - "${SYNAPSE_FEDERATION_PORT:-8448}:8448" + volumes: + - ./matrix/synapse/homeserver.yaml:/data/homeserver.yaml:ro + - synapse_data:/data/media_store + - synapse_signing_key:/data/keys + depends_on: + postgres: + condition: service_healthy + synapse-db-init: + condition: service_completed_successfully + healthcheck: + test: ["CMD-SHELL", "curl -fSs http://localhost:8008/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - mosaic-network + labels: + com.mosaic.service: "matrix-synapse" + com.mosaic.description: "Matrix homeserver (dev)" + + # ====================== + # Element Web (Matrix Client) + # ====================== + element-web: + image: vectorim/element-web:latest + container_name: mosaic-element-web + restart: unless-stopped + ports: + - "${ELEMENT_PORT:-8501}:80" + volumes: + - ./matrix/element/config.json:/app/config.json:ro + depends_on: + synapse: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - mosaic-network + labels: + com.mosaic.service: "matrix-element" + com.mosaic.description: "Element Web client (dev)" + +volumes: + synapse_data: + name: mosaic-synapse-data + synapse_signing_key: + name: mosaic-synapse-signing-key diff --git a/docker/matrix/element/config.json b/docker/matrix/element/config.json new file mode 100644 index 0000000..509f013 --- /dev/null +++ b/docker/matrix/element/config.json @@ -0,0 +1,30 @@ +{ + "default_server_config": { + "m.homeserver": { + "base_url": "http://localhost:8008", + "server_name": "localhost" + } + }, + "brand": "Mosaic Stack Dev", + "default_theme": "dark", + "room_directory": { + "servers": ["localhost"] + }, + "features": { + "feature_video_rooms": false, + "feature_group_calls": false + }, + "show_labs_settings": true, + "piwik": false, + "posthog": { + "enabled": false + }, + "privacy_policy_url": null, + "terms_and_conditions_links": [], + "setting_defaults": { + "breadcrumbs": true, + "custom_themes": [] + }, + "disable_guests": true, + "disable_3pid_login": true +} diff --git a/docker/matrix/scripts/setup-bot.sh b/docker/matrix/scripts/setup-bot.sh new file mode 100755 index 0000000..59c541c --- /dev/null +++ b/docker/matrix/scripts/setup-bot.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# ============================================== +# Matrix Bot Account Setup Script +# ============================================== +# +# Creates the Mosaic bot user on the local Synapse instance and retrieves +# an access token. Idempotent — safe to run multiple times. +# +# Usage: +# docker/matrix/scripts/setup-bot.sh +# docker/matrix/scripts/setup-bot.sh --username custom-bot --password custom-pass +# +# Prerequisites: +# - Synapse must be running (docker compose -f ... up synapse) +# - Synapse must be healthy (check with: curl http://localhost:8008/health) +# +# Output: +# Prints the environment variables needed for MatrixService configuration. +# +# ============================================== + +set -euo pipefail + +# Defaults +SYNAPSE_URL="${SYNAPSE_URL:-http://localhost:8008}" +BOT_USERNAME="${BOT_USERNAME:-mosaic-bot}" +BOT_PASSWORD="${BOT_PASSWORD:-mosaic-bot-dev-password}" +BOT_DISPLAY_NAME="${BOT_DISPLAY_NAME:-Mosaic Bot}" +ADMIN_USERNAME="${ADMIN_USERNAME:-admin}" +ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin-dev-password}" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --username) BOT_USERNAME="$2"; shift 2 ;; + --password) BOT_PASSWORD="$2"; shift 2 ;; + --synapse-url) SYNAPSE_URL="$2"; shift 2 ;; + --admin-username) ADMIN_USERNAME="$2"; shift 2 ;; + --admin-password) ADMIN_PASSWORD="$2"; shift 2 ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --username NAME Bot username (default: mosaic-bot)" + echo " --password PASS Bot password (default: mosaic-bot-dev-password)" + echo " --synapse-url URL Synapse URL (default: http://localhost:8008)" + echo " --admin-username NAME Admin username (default: admin)" + echo " --admin-password PASS Admin password (default: admin-dev-password)" + echo " --help, -h Show this help" + exit 0 + ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +echo "=== Mosaic Stack — Matrix Bot Setup ===" +echo "" +echo "Synapse URL: ${SYNAPSE_URL}" +echo "Bot username: ${BOT_USERNAME}" +echo "" + +# Wait for Synapse to be ready +echo "Checking Synapse health..." +for i in $(seq 1 30); do + if curl -fsSo /dev/null "${SYNAPSE_URL}/health" 2>/dev/null; then + echo "Synapse is healthy." + break + fi + if [ "$i" -eq 30 ]; then + echo "ERROR: Synapse is not responding at ${SYNAPSE_URL}/health after 30 attempts." + echo "Make sure Synapse is running:" + echo " docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml up -d" + exit 1 + fi + echo " Waiting for Synapse... (attempt ${i}/30)" + sleep 2 +done + +echo "" + +# Step 1: Register admin account (if not exists) +echo "Step 1: Registering admin account '${ADMIN_USERNAME}'..." +ADMIN_REGISTER_RESPONSE=$(curl -sS -X POST "${SYNAPSE_URL}/_synapse/admin/v1/register" \ + -H "Content-Type: application/json" \ + -d "{}" 2>/dev/null || true) + +NONCE=$(echo "${ADMIN_REGISTER_RESPONSE}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('nonce',''))" 2>/dev/null || true) + +if [ -n "${NONCE}" ]; then + # Generate HMAC for admin registration using the nonce + # For dev, we use register_new_matrix_user via docker exec instead + echo " Using docker exec to register admin via Synapse CLI..." + docker exec mosaic-synapse register_new_matrix_user \ + -u "${ADMIN_USERNAME}" \ + -p "${ADMIN_PASSWORD}" \ + -a \ + -c /data/homeserver.yaml \ + http://localhost:8008 2>/dev/null && echo " Admin account created." || echo " Admin account already exists (or registration failed — continuing)." +else + echo " Attempting registration via docker exec..." + docker exec mosaic-synapse register_new_matrix_user \ + -u "${ADMIN_USERNAME}" \ + -p "${ADMIN_PASSWORD}" \ + -a \ + -c /data/homeserver.yaml \ + http://localhost:8008 2>/dev/null && echo " Admin account created." || echo " Admin account already exists (or registration failed — continuing)." +fi + +echo "" + +# Step 2: Get admin access token +echo "Step 2: Obtaining admin access token..." +ADMIN_LOGIN_RESPONSE=$(curl -sS -X POST "${SYNAPSE_URL}/_matrix/client/v3/login" \ + -H "Content-Type: application/json" \ + -d "{ + \"type\": \"m.login.password\", + \"identifier\": { + \"type\": \"m.id.user\", + \"user\": \"${ADMIN_USERNAME}\" + }, + \"password\": \"${ADMIN_PASSWORD}\" + }" 2>/dev/null) + +ADMIN_TOKEN=$(echo "${ADMIN_LOGIN_RESPONSE}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || true) + +if [ -z "${ADMIN_TOKEN}" ]; then + echo "ERROR: Could not obtain admin access token." + echo "Response: ${ADMIN_LOGIN_RESPONSE}" + echo "" + echo "Try registering the admin account manually:" + echo " docker exec -it mosaic-synapse register_new_matrix_user -u ${ADMIN_USERNAME} -p ${ADMIN_PASSWORD} -a -c /data/homeserver.yaml http://localhost:8008" + exit 1 +fi +echo " Admin token obtained." + +echo "" + +# Step 3: Register bot account via admin API (idempotent) +echo "Step 3: Registering bot account '${BOT_USERNAME}'..." +BOT_REGISTER_RESPONSE=$(curl -sS -X PUT "${SYNAPSE_URL}/_synapse/admin/v2/users/@${BOT_USERNAME}:localhost" \ + -H "Authorization: Bearer ${ADMIN_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"password\": \"${BOT_PASSWORD}\", + \"displayname\": \"${BOT_DISPLAY_NAME}\", + \"admin\": false, + \"deactivated\": false + }" 2>/dev/null) + +BOT_EXISTS=$(echo "${BOT_REGISTER_RESPONSE}" | python3 -c "import sys,json; d=json.load(sys.stdin); print('yes' if d.get('name') else 'no')" 2>/dev/null || echo "no") + +if [ "${BOT_EXISTS}" = "yes" ]; then + echo " Bot account '@${BOT_USERNAME}:localhost' is ready." +else + echo " WARNING: Bot registration response unexpected: ${BOT_REGISTER_RESPONSE}" + echo " Continuing anyway — bot may already exist." +fi + +echo "" + +# Step 4: Get bot access token +echo "Step 4: Obtaining bot access token..." +BOT_LOGIN_RESPONSE=$(curl -sS -X POST "${SYNAPSE_URL}/_matrix/client/v3/login" \ + -H "Content-Type: application/json" \ + -d "{ + \"type\": \"m.login.password\", + \"identifier\": { + \"type\": \"m.id.user\", + \"user\": \"${BOT_USERNAME}\" + }, + \"password\": \"${BOT_PASSWORD}\" + }" 2>/dev/null) + +BOT_TOKEN=$(echo "${BOT_LOGIN_RESPONSE}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || true) + +if [ -z "${BOT_TOKEN}" ]; then + echo "ERROR: Could not obtain bot access token." + echo "Response: ${BOT_LOGIN_RESPONSE}" + exit 1 +fi + +echo " Bot token obtained." +echo "" + +# Step 5: Output configuration +echo "============================================" +echo " Matrix Bot Setup Complete" +echo "============================================" +echo "" +echo "Add the following to your .env file:" +echo "" +echo " # Matrix Bridge Configuration" +echo " MATRIX_HOMESERVER_URL=http://localhost:8008" +echo " MATRIX_ACCESS_TOKEN=${BOT_TOKEN}" +echo " MATRIX_BOT_USER_ID=@${BOT_USERNAME}:localhost" +echo " MATRIX_SERVER_NAME=localhost" +echo "" +echo "Or, if running the API inside Docker (same compose network):" +echo "" +echo " MATRIX_HOMESERVER_URL=http://synapse:8008" +echo " MATRIX_ACCESS_TOKEN=${BOT_TOKEN}" +echo " MATRIX_BOT_USER_ID=@${BOT_USERNAME}:localhost" +echo " MATRIX_SERVER_NAME=localhost" +echo "" +echo "Element Web is available at: http://localhost:8501" +echo " Login with any registered user to test messaging." +echo "" +echo "Admin account: ${ADMIN_USERNAME} / ${ADMIN_PASSWORD}" +echo "Bot account: ${BOT_USERNAME} / ${BOT_PASSWORD}" +echo "============================================" diff --git a/docker/matrix/synapse/homeserver.yaml b/docker/matrix/synapse/homeserver.yaml new file mode 100644 index 0000000..304387c --- /dev/null +++ b/docker/matrix/synapse/homeserver.yaml @@ -0,0 +1,131 @@ +# ============================================== +# Synapse Homeserver Configuration — Development Only +# ============================================== +# +# This config is for LOCAL DEVELOPMENT with the Mosaic Stack docker-compose overlay. +# Do NOT use this in production. See docker-compose.sample.matrix.yml for production. +# +# Server name is set to 'localhost' — this is permanent and cannot be changed +# after the database has been initialized. +# +# ============================================== + +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "http://localhost:8008/" + +# ====================== +# Network Listeners +# ====================== +listeners: + # Client API (used by Element Web, Mosaic bridge, etc.) + - port: 8008 + tls: false + type: http + x_forwarded: true + bind_addresses: ["0.0.0.0"] + resources: + - names: [client, federation] + compress: false + +# ====================== +# Database (Shared PostgreSQL) +# ====================== +database: + name: psycopg2 + txn_limit: 10000 + args: + user: "synapse" + password: "synapse_dev_password" + database: "synapse" + host: "postgres" + port: 5432 + cp_min: 5 + cp_max: 10 + +# ====================== +# Media Storage +# ====================== +media_store_path: /data/media_store +max_upload_size: 50M +url_preview_enabled: true +url_preview_ip_range_blacklist: + - "127.0.0.0/8" + - "10.0.0.0/8" + - "172.16.0.0/12" + - "192.168.0.0/16" + - "100.64.0.0/10" + - "192.0.0.0/24" + - "169.254.0.0/16" + - "198.18.0.0/15" + - "::1/128" + - "fe80::/10" + - "fc00::/7" + - "2001:db8::/32" + - "ff00::/8" + - "fec0::/10" + +# ====================== +# Registration (Dev Only) +# ====================== +enable_registration: true +enable_registration_without_verification: true + +# ====================== +# Signing Keys +# ====================== +# Auto-generated on first startup and persisted in the signing_key volume +signing_key_path: "/data/keys/localhost.signing.key" + +# Suppress warning about trusted key servers in dev +suppress_key_server_warning: true +trusted_key_servers: [] + +# ====================== +# Room Configuration +# ====================== +enable_room_list_search: true +allow_public_rooms_over_federation: false + +# ====================== +# Rate Limiting (Relaxed for Dev) +# ====================== +rc_message: + per_second: 100 + burst_count: 200 + +rc_registration: + per_second: 10 + burst_count: 50 + +rc_login: + address: + per_second: 10 + burst_count: 50 + account: + per_second: 10 + burst_count: 50 + +# ====================== +# Logging +# ====================== +log_config: "/data/localhost.log.config" + +# Inline log config — write to stdout for docker logs +# Synapse falls back to a basic console logger if the log_config file is missing, +# so we leave log_config pointing to a non-existent file intentionally. +# Override: mount a custom log config file at /data/localhost.log.config + +# ====================== +# Miscellaneous +# ====================== +report_stats: false +macaroon_secret_key: "dev-macaroon-secret-change-in-production" +form_secret: "dev-form-secret-change-in-production" + +# Enable presence for dev +use_presence: true + +# Retention policy (optional, keep messages for 180 days in dev) +retention: + enabled: false