Files
Qortal-Nextcloud-Integration/scripts/ensure-nextcloud-talk-config.sh

595 lines
17 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "${script_dir}/.." && pwd)"
source "${script_dir}/lib-compose-project.sh"
compose_file=""
env_file=""
app_service="app"
verify_only="0"
usage() {
cat <<'USAGE'
Usage: ./scripts/ensure-nextcloud-talk-config.sh --compose-file <file> --env-file <file> [options]
Ensures Nextcloud Talk HPB/STUN/TURN/recording configuration matches env values,
then verifies the expected backends are present.
Options:
--compose-file <path> Docker compose file to use (required)
--env-file <path> Env file to use (required)
--app-service <name> Nextcloud app service name (default: app)
--verify-only Do not attempt repairs; only verify current state
-h, --help Show this help
USAGE
}
read_kv() {
local key="$1"
local line
line="$(grep -m1 -E "^${key}=" "${env_file}" || true)"
if [[ -z "${line}" ]]; then
return 1
fi
echo "${line#*=}"
}
trim_csv() {
local raw="${1:-}"
local cleaned
cleaned="$(echo "${raw}" | tr -d '[:space:]')"
cleaned="${cleaned#,}"
cleaned="${cleaned%,}"
echo "${cleaned}"
}
profile_enabled() {
local profiles="$1"
local name="$2"
[[ ",${profiles}," == *",${name},"* ]]
}
ensure_docker_access() {
local docker_path=""
docker_path="$(type -P docker || true)"
if [[ -z "${docker_path}" ]]; then
echo "docker is required"
exit 1
fi
if "${docker_path}" info >/dev/null 2>&1; then
DOCKER_CMD=("${docker_path}")
return 0
fi
if ! command -v sudo >/dev/null 2>&1; then
echo "docker is installed but this user cannot access it, and sudo is unavailable."
exit 1
fi
if sudo -n "${docker_path}" info >/dev/null 2>&1; then
DOCKER_CMD=(sudo "${docker_path}")
return 0
fi
echo "docker requires elevated privileges on this machine."
echo "You may be prompted for your sudo password."
if sudo "${docker_path}" info >/dev/null 2>&1; then
DOCKER_CMD=(sudo "${docker_path}")
return 0
fi
echo "Unable to access docker, even with sudo."
exit 1
}
docker() {
"${DOCKER_CMD[@]}" "$@"
}
compose_cmd() {
docker compose -f "${compose_file}" --env-file "${env_file}" "$@"
}
occ_exec() {
compose_cmd exec -T --user www-data "${app_service}" php occ "$@"
}
occ_has_command() {
local command_name="${1:-}"
if [[ -z "${command_name}" ]]; then
return 1
fi
occ_exec list 2>/dev/null | grep -qE "^[[:space:]]+${command_name}([[:space:]]|$)"
}
occ_exec_quiet_retry() {
local rc=1
local max_attempts="${OCC_RETRY_ATTEMPTS:-6}"
local retry_sleep="${OCC_RETRY_SLEEP_SECONDS:-4}"
local attempt
if [[ $# -eq 0 ]]; then
return 1
fi
for attempt in $(seq 1 "${max_attempts}"); do
occ_exec "$@" >/dev/null 2>&1
rc=$?
if [[ "${rc}" -eq 0 ]]; then
return 0
fi
sleep "${retry_sleep}"
done
return "${rc}"
}
capture_occ_retry() {
local out_file="$1"
shift
local rc=1
local max_attempts="${OCC_RETRY_ATTEMPTS:-6}"
local retry_sleep="${OCC_RETRY_SLEEP_SECONDS:-4}"
local attempt
: > "${out_file}"
if [[ $# -eq 0 ]]; then
return 1
fi
for attempt in $(seq 1 "${max_attempts}"); do
if occ_exec "$@" >"${out_file}" 2>/dev/null; then
return 0
fi
rc=$?
if [[ "${attempt}" -lt "${max_attempts}" ]]; then
sleep "${retry_sleep}"
fi
done
return "${rc}"
}
capture_occ() {
local out_file="$1"
shift
occ_exec "$@" >"${out_file}" 2>/dev/null || true
}
count_unique_urls() {
local file_path="$1"
grep -Eo 'https?://[^[:space:]|]+' "${file_path}" 2>/dev/null | sort -u | wc -l | tr -d '[:space:]'
}
json_escape() {
local value="${1:-}"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
value="${value//$'\n'/}"
printf '%s' "${value}"
}
normalize_recording_url() {
local value="${1:-}"
value="$(echo "${value}" | tr -d '\r' | xargs)"
value="${value%/}"
if [[ -z "${value}" ]]; then
echo ""
return 0
fi
echo "${value}/"
}
recording_servers_config_json() {
local recording_url="${1:-}"
local recording_secret="${2:-}"
local escaped_url escaped_secret
escaped_url="$(json_escape "${recording_url}")"
escaped_secret="$(json_escape "${recording_secret}")"
printf '{"servers":[{"server":"%s","verify":true}],"secret":"%s"}' "${escaped_url}" "${escaped_secret}"
}
detect_talk_occ_prefix() {
local attempts="${1:-8}"
local delay_seconds="${2:-3}"
local prefix=""
local _attempt
local list_file=""
list_file="$(mktemp)"
for _attempt in $(seq 1 "${attempts}"); do
if capture_occ_retry "${list_file}" list; then
if grep -qE "^[[:space:]]+talk:signaling:list([[:space:]]|$)" "${list_file}"; then
prefix="talk"
elif grep -qE "^[[:space:]]+spreed:signaling:list([[:space:]]|$)" "${list_file}"; then
prefix="spreed"
else
prefix=""
fi
else
prefix=""
fi
if [[ -n "${prefix}" ]]; then
rm -f "${list_file}" >/dev/null 2>&1 || true
echo "${prefix}"
return 0
fi
if [[ "${_attempt}" -lt "${attempts}" ]]; then
sleep "${delay_seconds}"
fi
done
rm -f "${list_file}" >/dev/null 2>&1 || true
return 1
}
detect_talk_recording_prefix() {
local attempts="${1:-8}"
local delay_seconds="${2:-3}"
local prefix=""
local _attempt
local list_file=""
list_file="$(mktemp)"
for _attempt in $(seq 1 "${attempts}"); do
if capture_occ_retry "${list_file}" list; then
if grep -qE "^[[:space:]]+talk:recording:add([[:space:]]|$)" "${list_file}"; then
prefix="talk"
elif grep -qE "^[[:space:]]+spreed:recording:add([[:space:]]|$)" "${list_file}"; then
prefix="spreed"
else
prefix=""
fi
else
prefix=""
fi
if [[ -n "${prefix}" ]]; then
rm -f "${list_file}" >/dev/null 2>&1 || true
echo "${prefix}"
return 0
fi
if [[ "${_attempt}" -lt "${attempts}" ]]; then
sleep "${delay_seconds}"
fi
done
rm -f "${list_file}" >/dev/null 2>&1 || true
return 1
}
wait_for_occ_command_listing() {
local attempts="${1:-8}"
local delay_seconds="${2:-3}"
local list_file=""
local _attempt
list_file="$(mktemp)"
for _attempt in $(seq 1 "${attempts}"); do
if capture_occ_retry "${list_file}" list; then
rm -f "${list_file}" >/dev/null 2>&1 || true
return 0
fi
if [[ "${_attempt}" -lt "${attempts}" ]]; then
sleep "${delay_seconds}"
fi
done
rm -f "${list_file}" >/dev/null 2>&1 || true
return 1
}
delete_extra_ids() {
local file_path="$1"
local keep="$2"
local delete_cmd="$3"
mapfile -t delete_ids < <(
awk -v keep="${keep}" '
{
if (keep != "" && index($0, keep) > 0) {
next;
}
if (match($0, /\|[[:space:]]*([0-9]+)[[:space:]]*\|/, m)) {
print m[1];
next;
}
if (match($0, /^[[:space:]]*([0-9]+)[[:space:]]+/, m)) {
print m[1];
next;
}
}
' "${file_path}" 2>/dev/null | sort -u
)
local item_id
for item_id in "${delete_ids[@]}"; do
if [[ -n "${item_id}" ]]; then
occ_exec "${delete_cmd}" "${item_id}" >/dev/null 2>&1 || true
fi
done
}
while [[ $# -gt 0 ]]; do
case "$1" in
--compose-file)
compose_file="${2:-}"
shift 2
;;
--compose-file=*)
compose_file="${1#*=}"
shift
;;
--env-file)
env_file="${2:-}"
shift 2
;;
--env-file=*)
env_file="${1#*=}"
shift
;;
--app-service)
app_service="${2:-}"
shift 2
;;
--app-service=*)
app_service="${1#*=}"
shift
;;
--verify-only)
verify_only="1"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
if [[ -z "${compose_file}" || -z "${env_file}" ]]; then
usage
exit 1
fi
if [[ "${compose_file}" != /* ]]; then
compose_file="${repo_root}/${compose_file}"
fi
if [[ "${env_file}" != /* ]]; then
env_file="${repo_root}/${env_file}"
fi
if [[ ! -f "${compose_file}" ]]; then
echo "Missing compose file: ${compose_file}"
exit 1
fi
if [[ ! -f "${env_file}" ]]; then
echo "Missing env file: ${env_file}"
exit 1
fi
export_compose_project_name_from_env "${env_file}" || true
ensure_docker_access
profiles="$(trim_csv "$(read_kv "COMPOSE_PROFILES" || true)")"
talk_profile_enabled="0"
turn_profile_enabled="0"
recording_profile_enabled="0"
if profile_enabled "${profiles}" "talk" || profile_enabled "${profiles}" "janus"; then
talk_profile_enabled="1"
fi
if profile_enabled "${profiles}" "turn"; then
turn_profile_enabled="1"
talk_profile_enabled="1"
fi
if profile_enabled "${profiles}" "recording"; then
recording_profile_enabled="1"
talk_profile_enabled="1"
fi
if [[ "${talk_profile_enabled}" != "1" && "${recording_profile_enabled}" != "1" ]]; then
echo "Talk profiles not enabled in ${env_file}; nothing to do."
exit 0
fi
if ! occ_exec status >/dev/null 2>&1; then
echo "Nextcloud OCC is not ready in service '${app_service}'."
exit 1
fi
if ! wait_for_occ_command_listing 8 3; then
echo "Nextcloud OCC command listing is not ready in service '${app_service}'."
exit 1
fi
status=0
limited_recording_verify="0"
talk_signaling_public_url="$(read_kv "TALK_SIGNALING_PUBLIC_URL" || true)"
talk_signaling_secret="$(read_kv "TALK_SIGNALING_SECRET" || true)"
talk_stun_server="$(read_kv "TALK_STUN_SERVER" || true)"
talk_turn_server="$(read_kv "TALK_TURN_SERVER" || true)"
talk_domain="$(read_kv "SIGNALING_DOMAIN" || true)"
if [[ -z "${talk_domain}" ]]; then
talk_domain="$(read_kv "TALK_DOMAIN" || true)"
fi
talk_turn_host="$(read_kv "TALK_TURN_HOST" || true)"
if [[ -z "${talk_turn_host}" ]]; then
talk_turn_host="$(read_kv "NEXTCLOUD_DOMAIN" || true)"
fi
talk_turn_port="$(read_kv "TALK_TURN_PORT" || true)"
talk_turn_port="${talk_turn_port:-3478}"
talk_turn_secret="$(read_kv "TALK_TURN_SECRET" || true)"
if [[ -z "${talk_turn_server}" ]]; then
talk_turn_server="${talk_turn_host}:${talk_turn_port}"
fi
talk_recording_url="$(read_kv "TALK_RECORDING_URL" || true)"
talk_recording_url="${talk_recording_url:-http://talk_recording:1234}"
talk_recording_secret="$(read_kv "TALK_RECORDING_SHARED_SECRET" || true)"
talk_recording_url_normalized="$(normalize_recording_url "${talk_recording_url}")"
if [[ "${verify_only}" != "1" ]]; then
occ_exec_quiet_retry app:enable --force spreed >/dev/null 2>&1 || true
occ_exec_quiet_retry app:install spreed >/dev/null 2>&1 || true
occ_exec_quiet_retry app:enable --force spreed >/dev/null 2>&1 || true
fi
talk_prefix="$(detect_talk_occ_prefix 8 3 || true)"
recording_prefix="$(detect_talk_recording_prefix 8 3 || true)"
recording_list_cmd=""
if occ_has_command "talk:recording:list"; then
recording_list_cmd="talk:recording:list"
elif occ_has_command "spreed:recording:list"; then
recording_list_cmd="spreed:recording:list"
fi
echo "Ensuring Nextcloud Talk configuration..."
echo " compose_file=${compose_file}"
echo " env_file=${env_file}"
echo " talk_profile_enabled=${talk_profile_enabled}"
echo " turn_profile_enabled=${turn_profile_enabled}"
echo " recording_profile_enabled=${recording_profile_enabled}"
echo " talk_prefix=${talk_prefix:-<missing>}"
echo " recording_prefix=${recording_prefix:-<missing>}"
if [[ -z "${talk_prefix}" ]]; then
echo "Talk OCC namespace not found (expected talk:* or spreed:*)."
exit 1
fi
if [[ "${verify_only}" != "1" ]] && occ_has_command "${talk_prefix}:signaling:verify-keys"; then
occ_exec "${talk_prefix}:signaling:verify-keys" --update >/dev/null 2>&1 || true
fi
tmp_dir="$(mktemp -d)"
trap 'rm -rf "${tmp_dir}"' EXIT
if [[ "${talk_profile_enabled}" == "1" ]]; then
if [[ -z "${talk_signaling_public_url}" || -z "${talk_signaling_secret}" ]]; then
echo "Missing TALK_SIGNALING_PUBLIC_URL or TALK_SIGNALING_SECRET while Talk profile is enabled."
status=1
else
signaling_file="${tmp_dir}/signaling.txt"
capture_occ "${signaling_file}" "${talk_prefix}:signaling:list"
if ! grep -Fq "${talk_signaling_public_url}" "${signaling_file}" 2>/dev/null; then
if [[ "${verify_only}" != "1" ]]; then
occ_exec "${talk_prefix}:signaling:add" "${talk_signaling_public_url}" "${talk_signaling_secret}" >/dev/null 2>&1 || true
fi
fi
capture_occ "${signaling_file}" "${talk_prefix}:signaling:list"
if occ_has_command "${talk_prefix}:signaling:delete"; then
delete_extra_ids "${signaling_file}" "${talk_signaling_public_url}" "${talk_prefix}:signaling:delete"
capture_occ "${signaling_file}" "${talk_prefix}:signaling:list"
fi
if ! grep -Fq "${talk_signaling_public_url}" "${signaling_file}" 2>/dev/null; then
echo "Talk signaling backend missing after ensure: ${talk_signaling_public_url}"
status=1
fi
signaling_count="$(count_unique_urls "${signaling_file}")"
if [[ -n "${signaling_count}" && "${signaling_count}" -gt 1 ]]; then
echo "Multiple signaling backends still configured (${signaling_count})."
status=1
fi
if [[ -n "${talk_stun_server}" ]]; then
stun_file="${tmp_dir}/stun.txt"
capture_occ "${stun_file}" "${talk_prefix}:stun:list"
if ! grep -Fq "${talk_stun_server}" "${stun_file}" 2>/dev/null; then
if [[ "${verify_only}" != "1" ]]; then
occ_exec "${talk_prefix}:stun:add" "${talk_stun_server}" >/dev/null 2>&1 || true
fi
fi
capture_occ "${stun_file}" "${talk_prefix}:stun:list"
if occ_has_command "${talk_prefix}:stun:delete"; then
delete_extra_ids "${stun_file}" "${talk_stun_server}" "${talk_prefix}:stun:delete"
capture_occ "${stun_file}" "${talk_prefix}:stun:list"
fi
if ! grep -Fq "${talk_stun_server}" "${stun_file}" 2>/dev/null; then
echo "Talk STUN server missing after ensure: ${talk_stun_server}"
status=1
fi
else
echo "Note: TALK_STUN_SERVER is empty; STUN verification skipped."
fi
fi
fi
if [[ "${turn_profile_enabled}" == "1" ]]; then
if [[ -z "${talk_turn_secret}" || -z "${talk_domain}" ]]; then
echo "Missing TALK_TURN_SECRET or SIGNALING_DOMAIN/TALK_DOMAIN while TURN profile is enabled."
status=1
else
turn_file="${tmp_dir}/turn.txt"
capture_occ "${turn_file}" "${talk_prefix}:turn:list"
if ! grep -Fq "${talk_turn_server}" "${turn_file}" 2>/dev/null; then
if [[ "${verify_only}" != "1" ]]; then
occ_exec "${talk_prefix}:turn:add" turn "${talk_turn_server}" udp,tcp --secret "${talk_turn_secret}" >/dev/null 2>&1 || true
fi
fi
capture_occ "${turn_file}" "${talk_prefix}:turn:list"
if occ_has_command "${talk_prefix}:turn:delete"; then
delete_extra_ids "${turn_file}" "${talk_turn_server}" "${talk_prefix}:turn:delete"
capture_occ "${turn_file}" "${talk_prefix}:turn:list"
fi
if ! grep -Fq "${talk_turn_server}" "${turn_file}" 2>/dev/null; then
echo "Talk TURN server missing after ensure: ${talk_turn_server}"
status=1
fi
fi
fi
if [[ "${recording_profile_enabled}" == "1" ]]; then
if [[ -z "${talk_recording_secret}" ]]; then
echo "Missing TALK_RECORDING_SHARED_SECRET while recording profile is enabled."
status=1
else
recording_apply_status=0
if [[ -n "${recording_prefix}" ]]; then
if [[ "${verify_only}" != "1" ]]; then
occ_exec "${recording_prefix}:recording:add" "${talk_recording_url}" "${talk_recording_secret}" >/dev/null 2>&1
recording_apply_status=$?
if [[ "${recording_apply_status}" -ne 0 ]]; then
occ_exec "${recording_prefix}:recording:set" "${talk_recording_url}" "${talk_recording_secret}" >/dev/null 2>&1
recording_apply_status=$?
fi
fi
if [[ -n "${recording_list_cmd}" ]]; then
recording_file="${tmp_dir}/recording.txt"
capture_occ "${recording_file}" "${recording_list_cmd}"
if ! grep -Fq "${talk_recording_url}" "${recording_file}" 2>/dev/null \
&& ! grep -Fq "${talk_recording_url_normalized}" "${recording_file}" 2>/dev/null; then
echo "Talk recording backend missing after ensure: ${talk_recording_url}"
status=1
fi
else
limited_recording_verify="1"
echo "Note: recording list command unavailable; verification limited to add/set command success."
if [[ "${verify_only}" == "1" || "${recording_apply_status}" -ne 0 ]]; then
status=1
fi
fi
else
limited_recording_verify="1"
echo "Note: Talk recording OCC commands unavailable; falling back to spreed app config."
recording_config_json="$(recording_servers_config_json "${talk_recording_url_normalized}" "${talk_recording_secret}")"
if [[ "${verify_only}" != "1" ]]; then
occ_exec config:app:set spreed recording_servers --value="${recording_config_json}" >/dev/null 2>&1 || true
occ_exec config:app:set spreed call_recording --value="yes" >/dev/null 2>&1 || true
fi
recording_config_value="$(occ_exec config:app:get spreed recording_servers 2>/dev/null | tr -d '\r' || true)"
if [[ "${recording_config_value}" != *"${talk_recording_url_normalized}"* ]]; then
echo "Talk recording backend missing after config fallback: ${talk_recording_url_normalized}"
status=1
fi
fi
fi
fi
echo "Talk verification summary:"
echo " signaling_url=${talk_signaling_public_url:-<empty>}"
echo " turn_server=${talk_turn_server:-<empty>}"
echo " recording_url=${talk_recording_url:-<empty>}"
echo " limited_recording_verify=${limited_recording_verify}"
if [[ "${status}" -eq 0 ]]; then
echo "Talk configuration verified."
else
echo "Talk configuration verification failed."
fi
exit "${status}"