LibrePortal/containers/matrix/scripts/matrix_install_hooks.sh
librelad 751a4578d6 webui: trim the field tooltips added for the slot rename
Matches b562059 — the fourteen mapping entries added alongside the slot rename
were written before that landed and ran 88-100 chars against a median of 44.
2026-08-18 19:53:06 +01:00

239 lines
10 KiB
Bash

#!/bin/bash
# Matrix (Synapse + Element) install hooks.
#
# Synapse takes essentially no configuration from the environment — it reads
# homeserver.yaml and nothing else — so the real install work is done here:
# generate the signing key, write homeserver.yaml from the template with the
# server name and secrets filled in, write Element's config.json, and create the
# first admin account once the homeserver is answering.
matrix_install_pre()
{
local app_name="$1"
if ! appInstallCheckRequirements "$app_name" "$CFG_MATRIX_REQUIRES"; then
matrix=n
return 1
fi
}
# The homeserver's public host — the value that becomes server_name.
#
# Built from the port arrays and $domain_full that variables_init_app has
# already put in scope, NOT by reading DOMAINSUBNAME_TAG_1 out of the deployed
# compose: install_post_compose runs before dockerConfigSetupFileWithData, so at
# that point the compose still holds raw placeholders.
#
# port_subdomains[0] is CFG_MATRIX_PORT_1, the Synapse router — deliberately not
# $host_setup, which for this two-host app resolves to Element instead. The
# empty/@/root cases mirror tagsProcessorPortSubdomains so the name computed
# here and the Traefik rule generated later cannot drift apart.
_matrixServerName()
{
local sub="${port_subdomains[0]}"
[[ -z "$domain_full" ]] && return 1
if [[ "$sub" == "@" || "$sub" == "root" ]]; then
echo "$domain_full"
elif [[ -n "$sub" ]]; then
echo "${sub}.${domain_full}"
else
echo "matrix.${domain_full}"
fi
}
matrix_install_post_compose()
{
local app_name="$1"
local app_dir="$containers_dir$app_name"
local data_dir="$app_dir/data"
((menu_number++))
echo ""
echo "---- $menu_number. Generating the Synapse homeserver configuration"
echo ""
local server_name
server_name=$(_matrixServerName "$app_name")
if [[ -z "$server_name" ]]; then
isError "Could not determine the homeserver name from the compose file — aborting Synapse configuration."
isNotice "Check that CFG_MATRIX_PORT_1 is public and Traefik-managed, then reinstall."
return 1
fi
# Must match the password the compose handed to Postgres. Read it back from
# the deployed compose rather than from CFG_MATRIX_DB_PASSWORD_1: this hook
# runs after templating, so the compose is the settled value, and it stays
# correct even on an install whose config still holds the placeholder.
local db_password
db_password=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_DB_PASSWORD_1_TAG")
if [[ -z "$db_password" || "$db_password" == "MATRIX_DB_PASSWORD_1_DATA" ]]; then
isError "Database password was not generated in the compose file — aborting Synapse configuration."
return 1
fi
local result
result=$(createFolders "loud" "$docker_install_user" "$data_dir" "$app_dir/element")
checkSuccess "Creating $app_name data folders"
# Synapse signs every federated event with this key, and a peer that has
# seen one key will reject events signed by a different one. So: generate it
# exactly once, and never regenerate it over an existing install.
#
# Upstream's `generate` command is used rather than hand-rolling the key
# file, because the format encodes a key ID that other servers cache. It
# also emits a homeserver.yaml and a log config, which we throw away in
# favour of the templates below.
if [[ ! -s "$data_dir/signing.key" ]]; then
local synapse_image
synapse_image=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_VERSION_TAG")
synapse_image="matrixdotorg/synapse:${synapse_image:-latest}"
result=$(runFileOp docker run --rm \
-e SYNAPSE_SERVER_NAME="$server_name" \
-e SYNAPSE_REPORT_STATS=no \
-v "$data_dir":/data \
"$synapse_image" generate 2>&1)
checkSuccess "Generating the Synapse signing key with $synapse_image"
# `generate` names the key after the server; homeserver.yaml expects it
# at a fixed path so the file does not have to be renamed if the app is
# ever restored under a different name.
if [[ -f "$data_dir/$server_name.signing.key" ]]; then
result=$(runFileOp mv "$data_dir/$server_name.signing.key" "$data_dir/signing.key")
checkSuccess "Storing the signing key at data/signing.key"
fi
# Ours replace both of these.
result=$(runFileOp rm -f "$data_dir/homeserver.yaml" "$data_dir/$server_name.log.config")
checkSuccess "Discarding the generated config in favour of the LibrePortal template"
else
isNotice "An existing signing key was found — keeping it (regenerating would break federation)."
fi
if [[ ! -s "$data_dir/signing.key" ]]; then
isError "No signing key was produced — Synapse will not start. Check that the image could be pulled."
return 1
fi
result=$(copyResource "$app_name" "homeserver.yaml" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying homeserver.yaml to $data_dir"
result=$(copyResource "$app_name" "log.config" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying log.config to $data_dir"
local homeserver_file="$data_dir/homeserver.yaml"
# Three independent secrets, each generated fresh. registration_shared_secret
# can mint an account on this server, so it is as sensitive as an admin
# password — it is why homeserver.yaml is chmod 600 below.
local registration_secret macaroon_secret form_secret
registration_secret=$(openssl rand -hex 32)
macaroon_secret=$(openssl rand -hex 32)
form_secret=$(openssl rand -hex 32)
local enable_registration="false"
[[ "$CFG_MATRIX_ENABLE_REGISTRATION" == "true" ]] && enable_registration="true"
runFileOp sed -i "s|SYNAPSE_SERVER_NAME_PLACEHOLDER|$server_name|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_DB_PASSWORD_PLACEHOLDER|$db_password|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_REGISTRATION_SECRET_PLACEHOLDER|$registration_secret|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_MACAROON_SECRET_PLACEHOLDER|$macaroon_secret|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_FORM_SECRET_PLACEHOLDER|$form_secret|g" "$homeserver_file"
runFileOp sed -i "s|SYNAPSE_ENABLE_REGISTRATION_PLACEHOLDER|$enable_registration|g" "$homeserver_file"
checkSuccess "Writing homeserver.yaml (server_name=$server_name registration=$enable_registration)"
runFileOp chmod 600 "$homeserver_file" "$data_dir/signing.key"
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$data_dir"
checkSuccess "Restricting permissions on the Synapse secrets"
# Element is a static bundle; config.json is the only thing that makes it
# point at this homeserver rather than matrix.org.
result=$(copyResource "$app_name" "element-config.json" "element" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1)
checkSuccess "Copying Element configuration to $app_dir/element"
result=$(runFileOp mv "$app_dir/element/element-config.json" "$app_dir/element/config.json")
checkSuccess "Renaming Element configuration to config.json"
runFileOp sed -i "s|ELEMENT_HOMESERVER_PLACEHOLDER|$server_name|g" "$app_dir/element/config.json"
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$app_dir/element"
checkSuccess "Pointing Element at https://$server_name"
}
matrix_install_post_start()
{
local app_name="$1"
((menu_number++))
echo ""
echo "---- $menu_number. Creating the first Matrix admin account"
echo ""
# Synapse runs its database migrations on first boot, which on an empty
# Postgres takes appreciably longer than the container takes to start.
# /health answers only once it is actually serving.
#
# Probed with python rather than curl: the Synapse image is debian-slim with
# no curl or wget in it, but python is what Synapse itself runs on, so it is
# always there.
local attempts=0
while ((attempts < 60)); do
if runFileOp docker exec matrix-synapse python -c \
"import urllib.request; urllib.request.urlopen('http://localhost:8008/health', timeout=5)" >/dev/null 2>&1; then
break
fi
sleep 2
((attempts++))
done
if ((attempts >= 60)); then
isError "Synapse did not become ready in time — no admin account was created."
isNotice "Check 'docker logs matrix-synapse'. Once it is up, create the account with:"
isNotice " docker exec -it matrix-synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008"
return 0
fi
local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}"
local admin_pass="${CFG_MATRIX_ADMIN_PASSWORD_1}"
if [[ -z "$admin_pass" || "$admin_pass" == RANDOMIZEDPASSWORD* ]]; then
isNotice "No admin password is set in matrix.config — skipping admin account creation."
return 0
fi
# Idempotent in practice: on a reinstall over existing data the account
# already exists and register_new_matrix_user fails with "User ID already
# taken", which is not worth failing the install over.
local result
result=$(runFileOp docker exec matrix-synapse register_new_matrix_user \
-u "$admin_user" -p "$admin_pass" -a \
-c /data/homeserver.yaml http://localhost:8008 2>&1)
if [[ "$result" == *"already taken"* ]]; then
isNotice "Matrix admin '$admin_user' already exists — leaving the existing account alone."
else
checkSuccess "Creating Matrix admin account '$admin_user'"
fi
}
matrix_install_post()
{
local app_name="$1"
local server_name
server_name=$(_matrixServerName "$app_name")
local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}"
echo ""
isNotice "Matrix homeserver:"
echo ""
echo " Server name : ${server_name}"
echo " Your user ID : @${admin_user}:${server_name}"
echo " Password : ${CFG_MATRIX_ADMIN_PASSWORD_1}"
echo ""
echo " Sign in through the Element web interface, or any Matrix client"
echo " (Element mobile/desktop, FluffyChat, Nheko) using the server name"
echo " above."
echo ""
echo " Registration is ${CFG_MATRIX_ENABLE_REGISTRATION:-false}. To invite"
echo " others while it stays closed, create their accounts with:"
echo " docker exec -it matrix-synapse register_new_matrix_user \\"
echo " -c /data/homeserver.yaml http://localhost:8008"
echo ""
}