update mastodon+janusxr+ configuration

This commit is contained in:
Leon van Kammen 2026-01-19 19:56:11 +01:00
parent 7a532ed9bc
commit 53ccca3c48
5 changed files with 181 additions and 67 deletions

View file

@ -53,7 +53,7 @@ $ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./config:/c
| `APPNAME` | `XRForge` | manyfold instance name |
| `HOMEPAGE` | `/models` | show '/models' URL as homepage (use `/` for manyfold default) |
| `THEME` | `default` | bootstrap theme |
| 'JANUSXR' | `` | run local JanusXR stack (janus-server, janus-gateway, janusweb) |
| 'IMPORT_INSTANCES' | `1` | integrate cherrypicked content from other JanusXR instances (see [manyfold/root/instances](root/instances) |
| `AFRAME_VERSION` | `1.7.0` | AFRAME version |
| `GODOT_VERSION` | `4.4.1-stable`| godot editor version |
| `GODOT_TEMPLATE_ZIP` | `` | godot template zip URL or file (default is empty godot project) |
@ -74,6 +74,8 @@ $ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./config:/c
| `FEDERATE_DRIVE_CACHE`| `1m0s` | specify interval to re-check all models/directories |
| `FEDERATE_DRIVE_KEY` | `` | specify path to TLS PEM private key file (`-v ./key.pem:/key.pem -e FEDERATE_DRIVE_KEY=/key.pem` dockerflag e.g.) |
| `FEDERATE_DRIVE_CERT` | `` | specify path to TLS PEM public key certificate/CA/intermediate file (`-v ./cert.pem:/cert.pem -e FEDERATE_DRIVE_KEY=/cert.pem` dockerflag e.g.) |
| `SERVER_CORS` | `http://localhost:5577`| CORS-ANYWHERE server for JanusWeb |
| `SERVER_JANUS` | `http://localhost:5566`| JANUS presence server for JanusWeb |
> NOTE: if you have nix installed, you can easily try out environment-flags by running: `docker load < $(nix-build nix/docker.nix) && manyfold/cli/manyfold run -e RUNTESTS=1` e.g.
@ -135,8 +137,9 @@ The quickest way is:
* TIP2: use env-var `RCLONE_REMOTE` to mount only one specific remote (in case of a [combined](https://rclone.org/combine/) or [union](https://rclone.org/union/) rclone remote e.g.).
* TIP2: use **alphanumeric** names for rclone remotes (manyfold libraries choke on dot- or other special-characters)
By default environment-flag `FEDERATE_DRIVE_PATH` will share path `/mnt/experiences` as an open web directory.
Make sure that the URL (and credentials if configure) of step 3 are setup properly, so it matches your reverse proxy/ or SSL configuration (via `FEDERATE_DRIVE_CERT` and `FEDERATE_DRIVE_KEY` flags)
By default environment-flag `FEDERATE_DRIVE_PATH` will share path `/mnt/experiences` as an open web directory.<br>
Make sure that the URL (and credentials if configure) of step 3 are setup properly, so it matches your reverse proxy/ or SSL configuration (via `FEDERATE_DRIVE_CERT` and `FEDERATE_DRIVE_KEY` flags).<br>
For more info see the SSL section below.
# Git libraries
@ -192,29 +195,46 @@ $ manyfold/cli/manyfold.sh run -e DEV=1
# JanusXR
When running xrforge with the `JANUSXR=1` env-flag, the opensource [JanusXR](https://janusxr.org) stack will be installed and started:
By default the xrforge docker will run an opensource [JanusXR](https://janusxr.org) stack in the background:
* [janus-server](https://github.com/janusvr/janus-server) for chat + syncing avatar positions
* [janus-gateway](https://github.com/meetecho/janus-gateway) for video/audio
* [janusweb](https://github.com/meetecho/janus-gateway) the viewer using the above services
* [janus-server](https://github.com/janusvr/janus-server) at port `5566`, for chat + syncing avatar positions
* [cors-anywhere](https://github.com/Rob--W/cors-anywhere) at port `5577`for a hasslefree deep immersive web
* TODO: [janus-gateway](https://github.com/meetecho/janus-gateway) for video/audio
> NOTE: consider this a fingers-crossed 'rolling release' installation, as this is not officially part of XRForge (just a helper for intranets).
For SSL, you need to configure your reverse proxy as following:
Note that janus-server exposes a http websocket at port 5566, so you need to configure your reverse proxy as following:
* wss://presence.foo.bar.com => localhost:5566
* https://cors.foo.bar.com > localhost:5577
* https://presence.foo.bar.com => 5566
> This assumes environment-var `FEDERATE_DRIVE_HOST` is set to `https://foo.bar.com` (`presence` / `cors` subdomain is automatically prefixed when running xrforge over https)
> This assumes environment-var `FEDERATE_DRIVE_HOST` is set to `https://foo.bar.com` (`presence` subdomain is automatically prefixed by the installer)
#### persist JanusXR stack
When running the container run the following cmds to speed up the boot-time:
# Developing
```
$ docker cp xrforge:/mnt/janusweb .
$ docker cp xrforge:/root/janus-server .
$ podman load < $(nix-build nix/docker.nix) # build xrforge-overlay on manyfold-image
$ $(./manyfold/cli/manyfold.sh run -e DEV=1) # start docker
```
then add the following flags to your docker cmd: `-v ./janusweb:/mnt/janusweb -v ./janus-server:/root/janus-server`
> Profit! now point your browser to 'https://localhost:8080'
> That way JanusXR does not have to be installed every time during boot
### With SSL (for WebXR/JanusXR testing)
> NOTE: WebXR requires SSL, so we need ngrok or [caddy](https://caddyserver.com) to run the docker over SSL:
```
$ $(CADDY=10.171.13.61 ./manyfold/cli/manyfold.sh run -e DEV=1)
```
make sure you have caddy installed before running this as root:
```
# /manyfold/cli/manyfold caddy 10.171.13.61
```
> Profit! now point your browser to `https://<external_ip>`
you can edit/copy files via:
* `podman exec -it xrforge vim /manyfold/cli/manyfold.sh` e.g.
* `podman cp xrforge:/foo .` and vice versa `podman cp foo xrforge:/.` e.g.

View file

@ -6,7 +6,16 @@ test -n "$UPLOAD_PATH" || export UPLOAD_PATH=/mnt/experiences
test -n "$THEME" || export THEME=slate
test -n "$HOMEPAGE" || export HOMEPAGE=/models
test -n "$GODOT_VERSION" || export GODOT_VERSION=4.4.1-stable
test -n "$CORS_PROXY" || export CORS_PROXY=1
test -n "$IMPORT_INSTANCES" || export IMPORT_INSTANCES=1
test -n "$SERVER_CORS" || export SERVER_CORS=http://localhost:5577
test -n "$SERVER_JANUS" || export SERVER_JANUS=http://localhost:5566
test -n "$HTTPS_ONLY" || export HTTPS_ONLY=disabled
test -n "$CADDY" && {
export HTTPS_ONLY=enabled
export SERVER_CORS=https://$CADDY:5577
export SERVER_JANUS=https://$CADDY:5566
export PUBLIC_HOSTNAME=localhost
}
db=/config/manyfold.sqlite3
# utility funcs
@ -29,15 +38,25 @@ run(){
#-e PUBLIC_HOSTNAME=localhost \
#-e PUBLIC_PORT=80 \
#-e JANUSXR=1 \
echo ${oci} run "$@" -p 8790:3214 -p 8791:3215 -p 5566:5566 -p 5577:5577 --name xrforge \
ports="-p 8080:3214 -p 8081:8081 -p 5566:5566 -p 5577:5577"
# shift ports with 1000 if caddy is used as SSL reverse proxy
test -n "$CADDY" && {
ports='-p 8080:3214 -p 9081:8081 -p 6566:5566 -p 6577:5577'
}
echo ${oci} run "$@" ${ports} --name xrforge \
-e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l \
-e DATABASE_ADAPTER=sqlite3 \
-e FEDERATE_DRIVE_HOST=http://localhost:8791 \
-e FEDERATE_DRIVE_HOST=http://localhost:8081 \
-e PUBLIC_HOSTNAME=$PUBLIC_HOSTNAME \
-e SERVER_JANUS=$SERVER_JANUS \
-e SERVER_CORS=$SERVER_CORS \
-e SUDO_RUN_UNSAFELY=enabled \
-e MULTIUSER=enabled \
-e FEDERATION=enabled \
-e THEME=$THEME \
-e HOMEPAGE=$HOMEPAGE \
-e UPLOAD_PATH=$UPLOAD_PATH \
-e GODOT_VERSION=4.4.1-stable \
-e FEDERATE_DRIVE_CACHE=5s \
--cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \
@ -48,8 +67,13 @@ run(){
overlayfs(){
test -d /manyfold || return 0; # nothing to override
echocolor "[$APPNAME]" "applying filesystem overlay"
# compose overlays
cd /
mkdir /usr/src/app/public/view || true
rsync -ravuzi /nix/store/*-janus/janusweb/* /usr/src/app/public/view/.
cd /manyfold
rsync -rvzi * /.
rsync -rvziL * /.
ln -fs /usr/src/app/public/view /mnt/view # /mnt is one big zippable runnable backup
#apply_patches
}
@ -79,7 +103,7 @@ hook(){
test -d ~/hook.d/$cmd && {
find -L ~/hook.d/$cmd/ -type f -executable -maxdepth 1 | sort -V | while read hook; do
logger " |+ hook $hook $*"
{ $hook "$@" || true; } 2>&1 | awk '{ gsub(/\/root\/\//,"",$1); $1 = sprintf("%-40s", $1)} 1' | logger
{ $hook "$@" 2>&1 || true; } | awk '{ gsub(/\/root\/\//,"",$1); $1 = sprintf("%-40s", $1)} 1' | logger
done
}
}
@ -142,14 +166,13 @@ set_upload_path(){
echocolor "[$APPNAME]" "configuring upload library"
test -d $UPLOAD_PATH || mkdir $UPLOAD_PATH
add_lib_to_db $UPLOAD_PATH
id=$(sqlite3 $db "select id from libraries where path = '$UPLOAD_PATH';")
set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))"
sqlite3 $db "INSERT INTO settings VALUES(6,'default_library',$id);
INSERT INTO sqlite_sequence VALUES('settings',6);"
set_global default_library "replace('--- $id\\n','\\n',char(10))"
}
mount_dir(){
find /mnt -type d -mindepth 1 -maxdepth 1 | grep -v janusweb | while read dir; do
find /mnt -type d -mindepth 1 -maxdepth 1 | grep -vE '(janusweb|view)' | while read dir; do
echocolor "[$APPNAME]" "mounting $dir as library"
add_lib_to_db "$dir"
done
@ -214,8 +237,25 @@ rename_app(){
echocolor "[$APPNAME]" "renaming manyfold to $APPNAME"
sed -i 's/title: Manyfold/title: '$APPNAME'/g' /usr/src/app/config/locales/*.yml
sed -i 's|powered_by_html:.*|powered_by_html: Radically opensource-powered by <a href="https://manifold.app" target="_blank">Manyfold</a>, <a href="https://xrfragment.org">XR Fragments</a>, <a href="https://github.com/jbaicoianu/janusweb" target="_blank">JanusWeb</a> and <a href="https://nixos.org" target="_blank">NIX</a>|g' /usr/src/app/config/locales/*.yml
sed -i 's|Models|Experiences|g' /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml
sed -i 's|Model|Experience|g' /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml
echocolor "[$APPNAME]" "renaming 'model' to 'experience'"
for dir in /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml; do
sed -i 's|Models|Experiences|g' "$dir"
sed -i 's|Model|Experience|g' "$dir"
#sed -i 's|model|experience|g' "$dir"
#sed -i 's|models|experiences|g' "$dir"
done
sed -i 's|File(s) uploaded succesfully|File(s) uploaded succesfully. The experience is now being (re)generated, please be patient and check back later|g' /usr/src/app/config/locales/models/en.yml
sed -i 's|File(s) uploaded succesfully|File(s) uploaded succesfully. The experience is now being (re)generated, please be patient and check back later|g' /usr/src/app/config/locales/model_files/en.yml
}
allow_csp(){
test -n "$FEDERATE_DRIVE_HOST" || return 0
# allow iframes + other federated servers
servers='"https://web.janusxr.org", "https://vesta.janusxr.org"'
sed -i "s|:self,|:self, $servers, ENV['FEDERATE_DRIVE_HOST'], ENV['SERVER_CORS'], ENV['SERVER_JANUS'],|g" /usr/src/app/app/controllers/application_controller.rb
sed -i "s|content_security_policy.connect_src(\*origins)|content_security_policy.connect_src(\*origins);content_security_policy.connect_src( $servers, ENV['FEDERATE_DRIVE_HOST'], ENV['SERVER_CORS'], ENV['SERVER_JANUS'])|g" /usr/src/app/app/controllers/application_controller.rb
# content_security_policy.clear_directives()
# content_security_policy.default_src '*'
}
start_syslog(){
@ -253,7 +293,10 @@ force_public(){
import_assets(){
test -n "$NO_ASSETS" && return 0 # nothing to do here
add_lib_to_db /mnt/asset
# mount because rclone does support symlinks outside of the served folder
mkdir /mnt/templates && mount --bind /nix/store/*-xrfragments/xrf/templates /mnt/templates
mkdir /mnt/assets && mount --bind /nix/store/*-xrfragments/xrf/assets /mnt/assets
add_lib_to_db /mnt/assets
add_lib_to_db /mnt/templates
}
@ -267,31 +310,11 @@ janusxr(){
cd /root/corsanywhere
npm install
PORT=5577 start_server ~/.run-corsanywhere node server.js &
PORT=${SERVER_CORS/*:/} start_server ~/.run-corsanywhere node server.js &
cd /root
chmod +x janus_server-linux
PORT=5566 start_server ~/.run-janus-server ./janus_server-linux &
## we should do this in nix/docker.nix but the image gets into GB's :/
#which git || apk add git
#which janus || apk add janus-gateway
#which node || apk add nodejs
#which bash || apk add bash
#cd /root
## install server
#test -d janus-server || git clone --depth 1 https://github.com/janusvr/janus-server
#cd janus-server
#test -d node_modules || { apk add npm && npm install; }
#test -f config.js || ln -f /root/.config/janus-server/config.js .
#start_server(){
# while sleep 2s; do
# flock -n ~/.janus-server node server.js
# echocolor "[janus-server]" "'node server.js' exited (why?)...restarting"
# done
#}
#test -f ~/.janus-server || start_server &
PORT=${SERVER_CORS/*:/} start_server ~/.run-janus-server ./janus_server-linux &
}
init_database(){
@ -302,7 +325,7 @@ init_database(){
set_global site_name "'$APPNAME'"
set_global site_tagline "'$TAGLINE'"
set_global model_tags_auto_tag_new "replace('--- \"\"\\n','\\n',char(10))"
set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))"
set_global model_path_template "replace('--- \"{creator}/{modelId}\"\\n','\\n',char(10))"
set_upload_path
#set_global about "$ABOUT"
import_assets
@ -311,17 +334,25 @@ init_database(){
touch ${db}.xrforgeinit
}
caddy(){
cat Caddyfile | sed 's|ip:|'$1':|g' > /tmp/Caddyfile
set -x
cat /tmp/Caddyfile
cd /tmp
$(which caddy) run
}
# The new entrypoint of the docker
boot(){
echocolor "[$APPNAME]" "booting..."
test -z "$NO_OVERLAYFS" && overlayfs
start_syslog
rename_app
allow_csp
set_homepage
start_hook_daemon
mount_rclone
janusxr
cp /root/templates/ARhome/* /mnt/janusweb/.
force_public &
# enable development mode (disables template caching etc)

View file

@ -0,0 +1,61 @@
#!/usr/bin/env ruby
require 'json'
require_relative './../../xrforge.rb'
# Check if a filename is provided
if ARGV.length != 1
puts "Usage: #{$0} <path/to/experience/datapackage.json>"
exit 1
end
filename = ARGV[0]
begin
# dont run for each file-update
if ! filename.end_with?("datapackage.json")
exit 0
end
# Change the directory
dir = File.dirname(filename)
dirPublic = dir.gsub("/mnt/","").gsub("#","%23")
Dir.chdir( File.dirname(filename) )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
XRForge.log("✅ starting generating audiovisual experience", logfile)
# Check if a model file was found after the loop
#model_file = XRForge.getExperienceFile(data,dir,logfile)
#if ! model_file
# XRForge.log("❌ No suitable 3D file found for JanusXR-compatible experience", logfile)
# exit
#end
# Get the value of the environment variable FEDERATE_DRIVE_HOST
federate_drive_host = ENV['FEDERATE_DRIVE_HOST']
autogenerate = true
if ! data['description']
end
XRForge.log("✅ generated audiovisual experience", logfile)
XRForge.log(" ", logfile)
# tag it!
if ! data['keywords'].include?('audiovisual')
data['keywords'].push('audiovisual')
File.write("datapackage.json", JSON.pretty_generate(data) )
end
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -2,6 +2,7 @@
require 'json'
require 'erb'
require 'cgi'
require_relative './../../xrforge.rb'
# Check if a filename is provided
@ -21,7 +22,6 @@ def addLink(url)
return url
end
begin
# dont run for each file-update
@ -49,9 +49,6 @@ begin
XRForge.log("❌ No suitable 3D file found for JanusXR-compatible experience", logfile)
end
# Get the value of the environment variable FEDERATE_DRIVE_HOST
federate_drive_host = ENV['FEDERATE_DRIVE_HOST']
autogenerate = true
janusweb_src = ENV['DEV'] ? "/janusweb/janusweb.js" : "/janusweb/janusweb.min.js"
@ -104,16 +101,19 @@ begin
xrf_base = model_file ? File.basename(model_file, xrf_ext) : ""
# remove the metadata from the description
description = data['description'].gsub(metadataRegex,"").strip()
description = CGI.escapeHTML(
data['description'].gsub(metadataRegex,"")
.strip()
)
data['resources'].each do |resource|
ext = File.extname(resource['path'])
if ext.match(/mp3/i)
if ext.match(/(mp3|ogg)/i)
audiovisual = model_file ? false : true
base = File.basename(resource['path'], ext)
soundtrackUrl = addLink("#{federate_drive_host}/#{dirPublic}/#{resource["path"]}")
assets += " <assetsound id=\"soundtrack\" src=\"#{soundtrack_url}\"/>\n"
soundtrackUrl = addLink("model_files/#{resource["path"]}")
assets += " <assetsound id=\"soundtrack\" src=\"#{soundtrackUrl}\"/>\n"
objects += "<sound id=\"soundtrack\" loop=\"true\"/>\n"
end
@ -124,7 +124,7 @@ begin
audiovisual = model_file ? false : true
base = File.basename(resource['path'], ext)
videoSrc = addLink("#{federate_drive_host}/#{dirPublic}/#{resource["path"]}")
videoSrc = addLink("model_files/#{resource["path"]}")
assets += "<assetimage id=\"image\" src=\"#{videoSrc}\" loop=\"true\" auto_play=\"true\"/>\n"
objects += " <Object cull_face=\"none\" id=\"rplane\" js_id=\"image\" lighting=\"false\" pos=\"0 6.5 5.5\" scale=\"4 4 1\" image_id=\"image\"/>\n"
end
@ -133,7 +133,7 @@ begin
if ext.match(/mp4/i) || ext.match(/webm/i)
audiovisual = model_file ? false : true
base = File.basename(resource['path'], ext)
videoSrc = addLink("#{federate_drive_host}/#{dirPublic}/#{resource["path"]}")
videoSrc = addLink("model_files/#{resource["path"]}")
assets += "<assetvideo id=\"video\" src=\"#{videoSrc}\" loop=\"true\" auto_play=\"true\"/>\n"
objects += " <Object cull_face=\"none\" id=\"rplane\" js_id=\"video\" lighting=\"false\" pos=\"0 6.5 5.5\" scale=\"4 4 1\" video_id=\"video\"/>\n"
end
@ -143,7 +143,7 @@ begin
# TODO: copy files from a heuristic-based folder instead hardcoding it here
startpos = "pos=\"0 0 -10\""
use_local_asset = "room5"
rplaneSrc = addLink("#{federate_drive_host}/#{dirPublic}/roundedplane.glb")
rplaneSrc = addLink("model_files/roundedplane.glb")
system("cp /root/templates/audiovisual/roundedplane.glb #{dir}/.")
assets += " <assetobject id=\"rplane\" src=\"#{rplaneSrc}\"/>\n"
objects += " <Object cull_face=\"none\" id=\"rplane\" js_id=\"text_rplane\" lighting=\"false\" pos=\"2.69 1.84 -3.37\" rotation=\"0 45 0\" scale=\"1.6 1 1\"/>\n"
@ -164,11 +164,11 @@ begin
end
if description && !data['keywords'].include?('nodescription')
objects += " <Paragraph pos=\"2.8 1 -3.21\" fwd=\"0 0 1\" lighting=\"false\" rotation=\"180 -45 180\" col=\"0.5 0.8 0.5\" scale=\"2 2 2\" font_size=\"50\" locked=\"false\">#{description}</Paragraph>\n"
objects += " <paragraph pos=\"2.8 1 -3.21\" fwd=\"0 0 1\" lighting=\"false\" rotation=\"180 -45 180\" col=\"0.5 0.8 0.5\" scale=\"2 2 2\" font_size=\"50\" locked=\"false\">#{description}</paragraph>\n"
end
if model_file
objectSrc = "#{federate_drive_host}/#{dirPublic}/#{model_file.gsub("#","%23")}"
objectSrc = "model_files/#{model_file.gsub("#","%23")}"
if ! $links.include?(objectSrc)
objectSrc = addLink(objectSrc)
assets += " <assetobject id=\"experience\" src=\"#{objectSrc}\"/>\n"

View file

@ -13,6 +13,7 @@ if ARGV.length != 1
end
filename = ARGV[0]
# dont run for each file-update
if ! filename.end_with?("datapackage.json")
@ -68,6 +69,7 @@ begin
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
data['keywords'].each do |tag|
puts tag
if tag.match?(/^@.*@.*\./) # scan for activitypub handle (@foo@mastodon.online e.g.)
generate(tag,filename,dir,data,logfile)
end