Last active
July 30, 2024 15:00
-
-
Save alansikora/30781082da4f09fe4392eef062b47058 to your computer and use it in GitHub Desktop.
Rocket.Chat Matrix Federation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# !/bin/bash | |
# Rocket.Chat Matrix Federation Setup | |
# Author: Alan Sikora <[email protected]> | |
# https://github.com/alansikora | |
PWD=$(pwd) | |
[[ $1 = "-dev" ]] && DEV=true || DEV=false | |
# Set image versions | |
ROCKETCHAT_IMAGE_TAG="5.4.0" | |
SYNAPSE_IMAGE_TAG="v1.71.0" | |
TRAEFIK_IMAGE_TAG="v2.9.4" | |
REDIS_IMAGE_TAG="6.2.7" | |
NGINX_IMAGE_TAG="1.23.2" | |
ELEMENT_IMAGE_TAG="v1.11.14" | |
# Set config | |
RC_BRIDGE_PORT=3300 | |
updateEnvFile () { | |
echo "" > .env | |
cat << EOF > .env | |
# Rocket.Chat CLI | |
RC_IMAGE=$RC_IMAGE | |
ROCKETCHAT_IMAGE_TAG=$ROCKETCHAT_IMAGE_TAG | |
SYNAPSE_IMAGE_TAG=$SYNAPSE_IMAGE_TAG | |
TRAEFIK_IMAGE_TAG=$TRAEFIK_IMAGE_TAG | |
REDIS_IMAGE_TAG=$REDIS_IMAGE_TAG | |
NGINX_IMAGE_TAG=$NGINX_IMAGE_TAG | |
ELEMENT_IMAGE_TAG=$ELEMENT_IMAGE_TAG | |
# Setup | |
SERVER_HOSTNAME=$SERVER_HOSTNAME | |
RC_BRIDGE_PORT=$RC_BRIDGE_PORT | |
# Let's Encrypt | |
LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL | |
# Matrix | |
AS_UNIQUE_ID=$AS_UNIQUE_ID | |
AS_HS_TOKEN=$AS_HS_TOKEN | |
AS_AS_TOKEN=$AS_AS_TOKEN | |
EOF | |
} | |
alpineImageOrNot () { | |
read -p "Do you with to use the alpine version (y/n) [n]: " | |
if [ "$REPLY" = "y" ]; then | |
RC_IMAGE="$RC_IMAGE-alpine" | |
fi; | |
} | |
imageLatestStable () { | |
RC_IMAGE="rocket.chat:$ROCKETCHAT_IMAGE_TAG" | |
if [ $DEV = true ]; then | |
alpineImageOrNot | |
fi | |
} | |
echo "" | |
echo "Rocket.Chat Matrix Federation Setup" | |
if [ $DEV = true ]; then | |
echo ">>>>>>>> DEV MODE ENABLED <<<<<<<<<" | |
fi | |
echo "" | |
# Check if docker is installed | |
DOCKER_INFO=$(docker info &> /dev/null) | |
RESULT=$? | |
if [ $RESULT -ne 0 ]; then | |
if [ $RESULT -eq 127 ]; then | |
echo "Please, install the latest docker version then run this script again:" | |
echo "https://docs.docker.com/engine/install/" | |
echo "" | |
exit 1 | |
else | |
echo "Looks like you might have docker installed, but the wrong permissions, try this:" | |
echo "" | |
echo "> sudo groupadd docker" | |
echo "> sudo usermod -aG docker $USER" | |
echo "> newgrp docker" | |
echo "" | |
echo "And run this command again." | |
echo "" | |
exit 1 | |
fi | |
fi | |
# Import env vars | |
if [ -f "$PWD/.env" ]; then | |
printf "Loading env vars...\n" | |
export $(grep -v '^#' .env | xargs) | |
echo "" | |
fi | |
# Generate application service data | |
if [ -z "$AS_UNIQUE_ID" ]; then | |
CURRENT=$(date +%s) | |
AS_UNIQUE_ID=${AS_UNIQUE_ID:=$(echo "unique_$CURRENT" | sha1sum | head -c 40)} | |
AS_HS_TOKEN=${AS_HS_TOKEN:=$(echo "hs_$CURRENT" | sha256sum | head -c 64)} | |
AS_AS_TOKEN=${AS_AS_TOKEN:=$(echo "as_$CURRENT" | sha256sum | head -c 64)} | |
fi | |
if [ -z "$SERVER_HOSTNAME" ]; then | |
read -p "Type your server's hostname (without https or trailing slashes): " | |
SERVER_HOSTNAME="$REPLY" | |
updateEnvFile | |
printf "\nPlease, create the following records pointing to your server's IP address:\n" | |
printf " - $SERVER_HOSTNAME\n" | |
printf " - synapse.$SERVER_HOSTNAME\n" | |
printf " - element.$SERVER_HOSTNAME\n" | |
printf " - traefik.$SERVER_HOSTNAME\n" | |
printf "\n" | |
fi | |
if [ -z "$LETSENCRYPT_EMAIL" ]; then | |
read -p "Type your email address, will be used to issue certificates: " | |
LETSENCRYPT_EMAIL="$REPLY" | |
updateEnvFile | |
printf "\n" | |
fi | |
if [ $DEV = true ]; then | |
printf "Which Rocket.Chat image you want to use? (current: "; | |
if [ -z "${RC_IMAGE}" ]; then | |
printf "none" | |
else | |
printf "$RC_IMAGE" | |
fi | |
printf ")\n" | |
RC_IMAGE_DEFAULT_OPTION=1 | |
if ! [ -z "${RC_IMAGE}" ]; then | |
printf "0) keep current\n" | |
RC_IMAGE_DEFAULT_OPTION=0 | |
fi; | |
printf "1) latest stable\n" | |
printf "2) latest dev\n" | |
printf "3) specific version\n" | |
printf "4) specific PR\n" | |
read -p "Pick an option [$RC_IMAGE_DEFAULT_OPTION]: " | |
case $REPLY in | |
1) | |
imageLatestStable | |
;; | |
2) | |
RC_IMAGE="rocket.chat:develop" | |
ROCKETCHAT_IMAGE_TAG="develop" | |
alpineImageOrNot | |
;; | |
3) | |
read -p "Type the version: " | |
RC_IMAGE="rocket.chat:$REPLY" | |
ROCKETCHAT_IMAGE_TAG="$REPLY" | |
;; | |
4) | |
read -p "Type the PR number: " | |
RC_IMAGE="ghcr.io/rocketchat/rocket.chat:pr-$REPLY" | |
ROCKETCHAT_IMAGE_TAG="pr-$REPLY" | |
;; | |
*) | |
if [ "$RC_IMAGE_DEFAULT_OPTION" = 1 ]; then | |
imageLatestStable | |
fi | |
esac | |
echo "" | |
else | |
imageLatestStable | |
fi | |
updateEnvFile | |
# Setup synapse | |
printf "Setting up Synapse...\n" | |
mkdir -p data/matrix/synapse | |
if [ ! -f "$PWD/data/matrix/synapse/homeserver.yaml" ]; then | |
docker run -it --rm \ | |
-v $(pwd)/data/matrix/synapse:/data \ | |
-e SYNAPSE_SERVER_NAME="$SERVER_HOSTNAME" \ | |
-e SYNAPSE_REPORT_STATS=yes \ | |
-e UID=1000 \ | |
-e GID=1000 \ | |
matrixdotorg/synapse:$SYNAPSE_IMAGE_TAG generate | |
fi | |
if ! grep -q "enable_registration: true" data/matrix/synapse/homeserver.yaml; then | |
sed -i '$ d' data/matrix/synapse/homeserver.yaml | |
tee -a data/matrix/synapse/homeserver.yaml > /dev/null <<EOF | |
app_service_config_files: | |
- /data/registration.yaml | |
retention: | |
enabled: true | |
enable_registration: true | |
enable_registration_without_verification: true | |
suppress_key_server_warning: true | |
federation_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' | |
- '169.254.0.0/16' | |
- '::1/128' | |
- 'fe80::/64' | |
- 'fc00::/7' | |
database: | |
name: psycopg2 | |
args: | |
user: synapse | |
password: itsasecret | |
database: synapse | |
host: postgres | |
cp_min: 5 | |
cp_max: 10 | |
redis: | |
enabled: true | |
host: redis | |
port: 6379 | |
allow_public_rooms_without_auth: true | |
allow_public_rooms_over_federation: true | |
# vim:ft=yaml | |
EOF | |
fi | |
tee data/matrix/synapse/registration.yaml > /dev/null <<EOF | |
id: rocketchat_$AS_UNIQUE_ID | |
hs_token: $AS_HS_TOKEN | |
as_token: $AS_AS_TOKEN | |
url: http://172.17.0.1:$RC_BRIDGE_PORT | |
sender_localpart: rocket.cat | |
de.sorunome.msc2409.push_ephemeral: true | |
namespaces: | |
users: | |
- exclusive: false | |
regex: .* | |
rooms: | |
- exclusive: false | |
regex: .* | |
aliases: | |
- exclusive: false | |
regex: .* | |
rocketchat: | |
homeserver_url: http://synapse:8008 | |
homeserver_domain: $SERVER_HOSTNAME | |
EOF | |
# Setup Nginx | |
printf "\nSetting up Nginx...\n" | |
mkdir -p data/matrix/nginx | |
> data/matrix/nginx/matrix.conf | |
tee -a data/matrix/nginx/matrix.conf > /dev/null <<EOF | |
server { | |
listen 80; | |
listen $RC_BRIDGE_PORT; | |
server_name $SERVER_HOSTNAME; | |
location /.well-known/matrix/server { | |
access_log off; | |
add_header Access-Control-Allow-Origin *; | |
default_type application/json; | |
return 200 '{"m.server": "synapse.$SERVER_HOSTNAME:443"}'; | |
} | |
location /.well-known/matrix/client { | |
access_log off; | |
add_header Access-Control-Allow-Origin *; | |
default_type application/json; | |
return 200 '{"m.homeserver": {"base_url": "https://synapse.$SERVER_HOSTNAME"}}'; | |
} | |
location / { | |
proxy_set_header Host \$host; | |
proxy_set_header X-Real-IP \$remote_addr; | |
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; | |
proxy_set_header X-Forwarded-Proto \$scheme; | |
proxy_pass http://rocketchat:3000; | |
proxy_read_timeout 90; | |
} | |
} | |
EOF | |
# Setup Traefik | |
printf "\nSetting up Traefik...\n" | |
mkdir -p data/traefik/config | |
# - Setup ACME | |
if [ ! -f "$PWD/data/traefik/acme.json" ]; then | |
echo "{}" > data/traefik/acme.json | |
chmod 0600 data/traefik/acme.json | |
fi | |
# - Setup config: middlewares | |
tee data/traefik/config/middlewares.yml > /dev/null <<'EOF' | |
http: | |
middlewares: | |
httpsredirect: | |
redirectScheme: | |
scheme: https | |
permanent: true | |
EOF | |
# - Setup config: routers | |
tee data/traefik/config/routers.yml > /dev/null <<'EOF' | |
http: | |
routers: | |
redirecttohttps: | |
entryPoints: | |
- "web" | |
middlewares: | |
- "httpsredirect" | |
rule: "HostRegexp(`{host:.+}`)" | |
service: "noop@internal" | |
EOF | |
# - Setup traefik.yml | |
tee data/traefik/traefik.yml > /dev/null <<EOF | |
entryPoints: | |
web: | |
address: ":80" | |
web-secure: | |
address: ":443" | |
api: | |
dashboard: true | |
insecure: true | |
providers: | |
file: | |
directory: "/config" | |
watch: true | |
docker: | |
endpoint: "unix:///var/run/docker.sock" | |
network: "federation" | |
watch: true | |
exposedByDefault: false | |
certificatesResolvers: | |
letsencrypt: | |
acme: | |
caServer: https://acme-v02.api.letsencrypt.org/directory | |
email: "$LETSENCRYPT_EMAIL" | |
storage: "/acme.json" | |
httpChallenge: | |
entryPoint: "web" | |
EOF | |
# Setup Element | |
printf "\nSetting up Element...\n" | |
mkdir -p data/element | |
tee data/element/config.json > /dev/null <<EOF | |
{ | |
"default_server_config": { | |
"m.homeserver": { | |
"base_url": "https://synapse.$SERVER_HOSTNAME", | |
"server_name": "$SERVER_HOSTNAME" | |
}, | |
"m.identity_server": { | |
"base_url": "https://vector.im" | |
} | |
}, | |
"brand": "Element", | |
"integrations_ui_url": "https://scalar.vector.im/", | |
"integrations_rest_url": "https://scalar.vector.im/api", | |
"integrations_widgets_urls": [ | |
"https://scalar.vector.im/_matrix/integrations/v1", | |
"https://scalar.vector.im/api", | |
"https://scalar-staging.vector.im/_matrix/integrations/v1", | |
"https://scalar-staging.vector.im/api", | |
"https://scalar-staging.riot.im/scalar/api" | |
], | |
"hosting_signup_link": "https://element.io/matrix-services?utm_source=element-web&utm_medium=web", | |
"bug_report_endpoint_url": "https://element.io/bugreports/submit", | |
"uisi_autorageshake_app": "element-auto-uisi", | |
"showLabsSettings": true, | |
"piwik": { | |
"url": "https://piwik.riot.im/", | |
"siteId": 1, | |
"policyUrl": "https://element.io/cookie-policy" | |
}, | |
"roomDirectory": { | |
"servers": [ | |
"matrix.org", | |
"gitter.im", | |
"libera.chat" | |
] | |
}, | |
"enable_presence_by_hs_url": { | |
"https://matrix.org": false, | |
"https://matrix-client.matrix.org": false | |
}, | |
"terms_and_conditions_links": [ | |
{ | |
"url": "https://element.io/privacy", | |
"text": "Privacy Policy" | |
}, | |
{ | |
"url": "https://element.io/cookie-policy", | |
"text": "Cookie Policy" | |
} | |
], | |
"hostSignup": { | |
"brand": "Element Home", | |
"cookiePolicyUrl": "https://element.io/cookie-policy", | |
"domains": [ | |
"matrix.org" | |
], | |
"privacyPolicyUrl": "https://element.io/privacy", | |
"termsOfServiceUrl": "https://element.io/terms-of-service", | |
"url": "https://ems.element.io/element-home/in-app-loader" | |
}, | |
"sentry": { | |
"dsn": "https://[email protected]/6", | |
"environment": "develop" | |
}, | |
"posthog": { | |
"projectApiKey": "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", | |
"apiHost": "https://posthog.element.io" | |
}, | |
"features": { | |
"feature_spotlight": true | |
}, | |
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx" | |
} | |
EOF | |
# Setup PostgresSQL | |
printf "\nSetting up PostgreSQL...\n" | |
mkdir -p data/postgres/data | |
# Setup Docker Compose | |
printf "\nSetting up Docker Compose...\n" | |
tee docker-compose.yml > /dev/null <<'EOF' | |
services: | |
traefik: | |
image: "traefik:${TRAEFIK_IMAGE_TAG}" | |
restart: "unless-stopped" | |
ports: | |
- "80:80" | |
- "443:443" | |
volumes: | |
- "/var/run/docker.sock:/var/run/docker.sock:ro" | |
- "./data/traefik/traefik.yml:/etc/traefik/traefik.yml:ro" | |
- "./data/traefik/config:/config:ro" | |
- "./data/traefik/acme.json:/acme.json" | |
labels: | |
- "traefik.enable=true" | |
- "traefik.http.services.traefik.loadbalancer.server.port=8080" | |
- "traefik.http.routers.traefik.rule=Host(`traefik.$SERVER_HOSTNAME`)" | |
- "traefik.http.routers.traefik.entrypoints=web-secure" | |
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt" | |
networks: | |
- internal | |
postgres: | |
image: "postgres:14" | |
restart: "unless-stopped" | |
environment: | |
POSTGRES_PASSWORD: itsasecret | |
POSTGRES_USER: synapse | |
POSTGRES_DB: synapse | |
POSTGRES_INITDB_ARGS: "--encoding='UTF8' --lc-collate='C' --lc-ctype='C'" | |
volumes: | |
- "./data/postgres/data:/var/lib/postgresql/data" | |
networks: | |
- internal | |
redis: | |
image: "redis:${REDIS_IMAGE_TAG}" | |
restart: "unless-stopped" | |
networks: | |
- internal | |
synapse: | |
image: "matrixdotorg/synapse:${SYNAPSE_IMAGE_TAG}" | |
restart: "unless-stopped" | |
environment: | |
SYNAPSE_CONFIG_DIR: "/data" | |
SYNAPSE_CONFIG_PATH: "/data/homeserver.yaml" | |
UID: "1000" | |
GID: "1000" | |
TZ: "America/New_York" | |
volumes: | |
- "./data/matrix/synapse:/data" | |
ports: | |
- 8008:8008 | |
- 8448:8448 | |
labels: | |
- "traefik.enable=true" | |
- "traefik.http.services.synapse.loadbalancer.server.port=8008" | |
- "traefik.http.routers.synapse.rule=Host(`synapse.$SERVER_HOSTNAME`)" | |
- "traefik.http.routers.synapse.entrypoints=web-secure" | |
- "traefik.http.routers.synapse.tls=true" | |
- "traefik.http.routers.synapse.tls.certresolver=letsencrypt" | |
networks: | |
- internal | |
nginx: | |
image: "nginx:${NGINX_IMAGE_TAG}" | |
restart: "unless-stopped" | |
volumes: | |
- "./data/matrix/nginx/matrix.conf:/etc/nginx/conf.d/matrix.conf" | |
labels: | |
- "traefik.enable=true" | |
- "traefik.http.services.nginx.loadbalancer.server.port=80" | |
- "traefik.http.routers.nginx.rule=Host(`$SERVER_HOSTNAME`)" | |
- "traefik.http.routers.nginx.entrypoints=web-secure" | |
- "traefik.http.routers.nginx.tls=true" | |
- "traefik.http.routers.nginx.tls.certresolver=letsencrypt" | |
networks: | |
- internal | |
element: | |
image: vectorim/element-web:${ELEMENT_IMAGE_TAG} | |
restart: unless-stopped | |
volumes: | |
- ./data/element/config.json:/app/config.json | |
labels: | |
- "traefik.enable=true" | |
- "traefik.http.services.element.loadbalancer.server.port=80" | |
- "traefik.http.routers.element.rule=Host(`element.$SERVER_HOSTNAME`)" | |
- "traefik.http.routers.element.entrypoints=web-secure" | |
- "traefik.http.routers.element.tls=true" | |
- "traefik.http.routers.element.tls.certresolver=letsencrypt" | |
networks: | |
- internal | |
rocketchat: | |
image: "${RC_IMAGE}" | |
command: > | |
bash -c | |
"for i in `seq 1 30`; do | |
node main.js && | |
s=$$? && break || s=$$?; | |
echo \"Tried $$i times. Waiting 5 secs...\"; | |
sleep 5; | |
done; (exit $$s)" | |
restart: unless-stopped | |
volumes: | |
- ./uploads:/app/uploads | |
- ./data/matrix/synapse/registration.yaml:/app/matrix-federation-config/registration.yaml | |
environment: | |
- PORT=3000 | |
- ROOT_URL=https://localhost:3000 | |
- MONGO_URL=mongodb://mongodb:27017/rocketchat?replicaSet=rs0&directConnection=true | |
- MONGO_OPLOG_URL=mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true | |
- NODE_ENV=production | |
# - REG_TOKEN=${REG_TOKEN} | |
depends_on: | |
- mongodb | |
ports: | |
- 3000:3000 | |
- 3300:3300 | |
networks: | |
- internal | |
# labels: | |
# - "traefik.enable=true" | |
# - "traefik.http.services.rc.loadbalancer.server.port=3000" | |
# - "traefik.http.routers.rc.rule=Host(`$SERVER_HOSTNAME`)" | |
# - "traefik.http.routers.rc.entrypoints=web-secure" | |
# - "traefik.http.routers.rc.tls=true" | |
# - "traefik.http.routers.rc.tls.certresolver=letsencrypt" | |
# - "traefik.http.services.bridge.loadbalancer.server.port=$RC_BRIDGE_PORT" | |
# - "traefik.http.routers.bridge.rule=Host(`$SERVER_HOSTNAME`)" | |
# - "traefik.http.routers.bridge.entrypoints=web-secure" | |
# - "traefik.http.routers.bridge.tls=true" | |
# - "traefik.http.routers.bridge.tls.certresolver=letsencrypt" | |
mongodb: | |
image: mongo:5.0 | |
restart: unless-stopped | |
volumes: | |
- ./data/mongo/db:/data/db | |
command: mongod --oplogSize 128 --replSet rs0 | |
labels: | |
- "traefik.enable=false" | |
networks: | |
- internal | |
# this container's job is just run the command to initialize the replica set. | |
# it will run the command and remove himself (it will not stay running) | |
mongo-init-replica: | |
image: mongo:5.0 | |
command: > | |
bash -c | |
"for i in `seq 1 30`; do | |
mongo mongodb/rocketchat --eval \" | |
rs.initiate({ | |
_id: 'rs0', | |
members: [ { _id: 0, host: 'localhost:27017' } ]})\" && | |
s=$$? && break || s=$$?; | |
echo \"Tried $$i times. Waiting 5 secs...\"; | |
sleep 5; | |
done; (exit $$s)" | |
depends_on: | |
- mongodb | |
networks: | |
- internal | |
networks: | |
internal: | |
attachable: true | |
EOF |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Rocket.Chat Matrix Federation Tester | |
# Author: Alan Sikora <[email protected]> | |
# https://github.com/alansikora | |
echo "" | |
echo "Rocket.Chat Matrix Federation Tester" | |
echo "" | |
checkURL () { | |
echo "" | |
printf "Checking $URL_TO_TEST..." | |
RESPONSE=$(curl -s -L $URL_TO_TEST) | |
if [[ "$RESPONSE" == *"$EXPECTED_URL_SUBSTRING"* ]]; then | |
echo "OK!" | |
else | |
echo "$URL_TO_TEST is not as expected" | |
exit 1 | |
fi | |
} | |
# Import env vars | |
if [ -f "$PWD/.env" ]; then | |
printf "Loading env vars...\n" | |
export $(grep -v '^#' .env | xargs) | |
fi | |
if [ -z "${SERVER_HOSTNAME}" ]; then | |
echo "You must run the setup script first and have a .env file on your directory" | |
exit 1 | |
fi | |
# Check if synapse URL is correct | |
URL_TO_TEST="https://synapse.$SERVER_HOSTNAME" | |
EXPECTED_URL_SUBSTRING="<h1>It works! Synapse is running</h1>" | |
checkURL | |
# Check if client's well known is correct | |
URL_TO_TEST="https://$SERVER_HOSTNAME/.well-known/matrix/client" | |
EXPECTED_URL_SUBSTRING="\"base_url\": \"https://synapse.$SERVER_HOSTNAME\"" | |
checkURL | |
# Check if servers's well known is correct | |
URL_TO_TEST="https://$SERVER_HOSTNAME/.well-known/matrix/server" | |
EXPECTED_URL_SUBSTRING="\"synapse.$SERVER_HOSTNAME:443\"" | |
checkURL | |
# Check if Element's URL is correct | |
URL_TO_TEST="https://element.$SERVER_HOSTNAME/" | |
EXPECTED_URL_SUBSTRING="<noscript>Sorry, Element requires JavaScript to be enabled.</noscript>" | |
checkURL | |
# Check if Rocket.Chat's URL is correct | |
URL_TO_TEST="https://$SERVER_HOSTNAME/" | |
EXPECTED_URL_SUBSTRING="<title>Rocket.Chat</title>" | |
checkURL |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To use the script, I had to add
version: '3.3'
to the top of the docker-compose file. Otherwise some options were not recognised. If this issue happens with multiple people, perhaps automate that in the script! Either way, thanks for the setup!