#!/bin/sh
# Video meetings: Jitsi Meet, switched on and off from the interface.
#
# nuxpbx runs without root and with NoNewPrivileges, so it cannot install a
# package. It writes what it wants into /var/lib/nuxpbx/meet-wanted, a path
# unit notices, and this runs as root and does it: one line, the meeting
# name (meet.<domain>), or nothing for off. What happened is written to
# /var/lib/nuxpbx-meet/status, a directory of root's: the daemon reads it and
# can put nothing there, so no link it plants can point a write by root
# anywhere.
#
# Jitsi comes from its own repository, download.jitsi.org, without
# jitsi-meet-web-config: that one brings nginx onto port 443, which is
# nuxpbx's. nuxpbx serves the meeting name itself, the web files out of
# /usr/share/jitsi-meet, the websockets passed on to prosody and the bridge.
#
# Debian and Ubuntu only: Jitsi is packaged for nothing else.
set -eu

# Both overridable only so dev/packaging.sh can run this against a temp dir.
wanted=${NUXPBX_MEET_WANTED:-/var/lib/nuxpbx/meet-wanted}
state=${NUXPBX_MEET_STATE:-/var/lib/nuxpbx-meet}
# The videobridge's media port. Its own default, 10000, is the first port of
# Asterisk's RTP range.
port=9999

if [ "$(id -u)" != 0 ]; then
	echo "run this as root" >&2
	exit 1
fi

mkdir -p "$state"
chmod 755 "$state"

say() {
	printf '%s\n%s\n' "$1" "$2" >"$state/status.new"
	chmod 644 "$state/status.new"
	mv "$state/status.new" "$state/status"
	echo "meet: $1: $2"
}

# The daemon's file, read and checked before anything is done with it. A link
# is not followed: it would have root read whatever it points at.
asked() {
	if [ -f "$wanted" ] && [ ! -L "$wanted" ]; then
		head -c 256 "$wanted" | head -1 | tr -d '[:space:]'
	fi
}
host=$(asked)

# A change made while this runs fires the path unit into a service that is
# still busy, and systemd drops it: stopping the bridge alone takes over a
# minute. So the file is read again at the end, and a different answer is
# done as well.
again() {
	[ "$(asked)" = "$host" ] || exec "$0"
}

case "$host" in
"") ;;
meet.*.*)
	case "$host" in
	*[!a-z0-9.-]* | *..* | *.)
		say failed "the meeting name is not a host name"
		exit 1
		;;
	esac
	;;
*)
	say failed "the meeting name has to start with meet."
	exit 1
	;;
esac

services="prosody jicofo jitsi-videobridge2"

if [ -z "$host" ]; then
	# Off is stopped, not removed: switched on again it is back in seconds,
	# and nothing anybody configured is lost.
	if [ -s "$state/enabled" ]; then
		say stopping "switching video meetings off"
	fi
	# Stopped and disabled as two steps: jicofo is an init script, and
	# `disable --now` hands those to systemd-sysv-install, which never stops
	# what is running.
	for service in $services; do
		systemctl stop "$service" >/dev/null 2>&1 || true
		systemctl disable "$service" >/dev/null 2>&1 || true
	done
	was=$(cat "$state/enabled" 2>/dev/null || true)
	rm -f "$state/enabled"
	systemctl restart nuxpbx-firewall 2>/dev/null || true
	say off "video meetings are switched off"
	# nuxpbx serves the meeting name and its certificate from the start of
	# its https listener, so it lets go of them by starting again. Calls are
	# Asterisk's and are not touched.
	[ -z "$was" ] || systemctl try-restart nuxpbx 2>/dev/null || true
	again
	exit 0
fi

if ! command -v apt-get >/dev/null 2>&1; then
	say failed "video meetings need Debian or Ubuntu"
	exit 1
fi

# Java for the bridge and the focus, beside Asterisk and PostgreSQL. Measured
# before the first install only: once it runs, its own use is in the figure.
if ! dpkg-query -W -f='${Status}' jitsi-videobridge2 2>/dev/null | grep -q 'ok installed'; then
	available=$(awk '/^MemAvailable:/ { print int($2 / 1024) }' /proc/meminfo)
	if [ "${available:-0}" -lt 3072 ]; then
		say failed "video meetings need 3 GB of free memory, this machine has ${available:-0} MB"
		exit 1
	fi
fi

say installing "installing Jitsi for $host"

# The key that signs the meeting links. nuxpbx reads it, nobody else.
if [ ! -s "$state/secret" ]; then
	umask 077
	head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' >"$state/secret.new"
	umask 022
	mv "$state/secret.new" "$state/secret"
fi
chown root:nuxpbx "$state/secret" 2>/dev/null || true
chmod 640 "$state/secret"
secret=$(cat "$state/secret")

keyring=/usr/share/keyrings/jitsi-keyring.gpg
if [ ! -s "$keyring" ]; then
	curl -fsSL https://download.jitsi.org/jitsi-key.gpg.key | gpg --dearmor >"$keyring.new"
	mv "$keyring.new" "$keyring"
fi
echo "deb [signed-by=$keyring] https://download.jitsi.org stable/" >/etc/apt/sources.list.d/jitsi-stable.list

# The answers the packages would ask for. cert-choice belongs to
# jitsi-meet-web-config, which is not installed, and jitsi-meet-prosody's
# postinst asks it anyway and fails without an answer. The certificate it
# makes is prosody's own; what a browser sees is nuxpbx's.
debconf-set-selections <<EOF
jitsi-videobridge2 jitsi-videobridge/jvb-hostname string $host
jitsi-meet-prosody jitsi-videobridge/jvb-hostname string $host
jicofo jitsi-videobridge/jvb-hostname string $host
jitsi-meet-web-config jitsi-meet/cert-choice select Generate a new self-signed certificate
jitsi-meet-tokens jitsi-meet-tokens/appid string nuxpbx
jitsi-meet-tokens jitsi-meet-tokens/appsecret password $secret
EOF

export DEBIAN_FRONTEND=noninteractive
# The nightly update or unattended-upgrades may hold the lock when somebody
# presses: waited for, not failed on.
lock="-o DPkg::Lock::Timeout=600"
if ! apt-get $lock update -q >/dev/null 2>"$state/apt.log" ||
	! apt-get $lock install -yq --no-install-recommends \
		jitsi-videobridge2 jicofo jitsi-meet-prosody jitsi-meet-web jitsi-meet-tokens \
		>>"$state/apt.log" 2>&1; then
	say failed "installing Jitsi failed, see $state/apt.log"
	exit 1
fi

# Debian 13's lua-inspect stops at Lua 5.3 and its prosody runs on 5.4, so
# the token modules fail to load and every room opens without a token check.
# The file is plain Lua and the same for both.
lua=$(head -1 /usr/bin/prosody | sed -n 's/.*lua\([0-9.]*\).*/\1/p')
if [ -n "$lua" ] && ! "lua$lua" -e 'require("inspect")' >/dev/null 2>&1; then
	for older in 5.3 5.2 5.1; do
		if [ -f "/usr/share/lua/$older/inspect.lua" ]; then
			mkdir -p "/usr/share/lua/$lua"
			ln -sf "/usr/share/lua/$older/inspect.lua" "/usr/share/lua/$lua/inspect.lua"
			break
		fi
	done
fi

prosody_conf=/etc/prosody/conf.avail/$host.cfg.lua

# The key the links are signed with, written into prosody on every run:
# jitsi-meet-tokens takes it from debconf at its first configure only, so a
# key made again later, or a package configured before, signs links prosody
# refuses as an invalid signature.
sed -i -e "s|^\([[:space:]]*\)app_id *=.*|\1app_id=\"nuxpbx\"|" \
	-e "s|^\([[:space:]]*\)app_secret *=.*|\1app_secret=\"$secret\"|" "$prosody_conf"

# Guests come in without an account: an anonymous host of their own, let into
# a room without a token, once somebody with one has opened it (jicofo below).
if ! grep -q "^VirtualHost \"guest.$host\"" "$prosody_conf"; then
	cat >>"$prosody_conf" <<EOF

-- nuxpbx: guests join without an account, after a host has opened the room.
VirtualHost "guest.$host"
    authentication = "jitsi-anonymous"
    c2s_require_encryption = false
EOF
fi
if ! grep -q 'token_verification_allowlist' "$prosody_conf"; then
	sed -i "/^Component \"conference.$host\" \"muc\"/a\\    token_verification_allowlist = { \"guest.$host\" }" "$prosody_conf"
fi

# The focus opens a room only for a token, and lets guests in after that.
cat >/etc/jitsi/jicofo/nuxpbx.conf <<EOF
jicofo.authentication {
  enabled = true
  type = JWT
  login-url = "$host"
}
EOF
if ! grep -q 'nuxpbx.conf' /etc/jitsi/jicofo/jicofo.conf; then
	echo 'include "nuxpbx.conf"' >>/etc/jitsi/jicofo/jicofo.conf
fi

# The bridge: media on $port, and its websocket on the loopback for nuxpbx to
# pass on under the meeting name.
cat >/etc/jitsi/videobridge/nuxpbx.conf <<EOF
videobridge {
  ice.udp.port = $port
  http-servers.public {
    host = 127.0.0.1
    port = 9090
  }
  websockets {
    enabled = true
    domains = ["$host:443"]
    tls = true
  }
}
EOF
if ! grep -q 'nuxpbx.conf' /etc/jitsi/videobridge/jvb.conf; then
	echo 'include "nuxpbx.conf"' >>/etc/jitsi/videobridge/jvb.conf
fi

# Java's heap, which both start at three gigabytes each by default: on the
# same machine as Asterisk and PostgreSQL that is a phone system swapping.
# Enough for a handful of rooms; whoever needs more sets it in these files.
for pair in jicofo/config:JICOFO_MAX_MEMORY=768m videobridge/config:VIDEOBRIDGE_MAX_MEMORY=1536m; do
	file=/etc/jitsi/${pair%%:*}
	line=${pair#*:}
	grep -q "^${line%%=*}=" "$file" 2>/dev/null || echo "$line" >>"$file"
done

for service in $services; do
	systemctl enable "$service" >/dev/null 2>&1 || true
	systemctl restart "$service"
done
was=$(cat "$state/enabled" 2>/dev/null || true)
echo "$host" >"$state/enabled"
systemctl restart nuxpbx-firewall 2>/dev/null || true
say ready "video meetings run at https://$host"
# And takes them up the same way, see above.
[ "$was" = "$host" ] || systemctl try-restart nuxpbx 2>/dev/null || true
again
