summaryrefslogtreecommitdiff
path: root/README.md
blob: 5ed8f532aa15702bc978f1307c5b072ab8ed9e05 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# Yesod

Small self-hosted VR rooms. Anyone can start a room in a world, and a few
people can sit around and talk. It runs in the headset's web browser
(WebXR), so there's nothing to install on a Quest, and everything, including
every library and model, is served from your own server. Nothing is logged
about visitors, and a relay can hide everyone's IP address from the others.

The name is the sphere of the Tree of Life that holds the astral plane: the
world of images, where people meet without their bodies.

It comes with five worlds:

- **Campfire:** a pine clearing under the real night sky.
- **Temple:** a Thelemic temple with the Gnostic Mass hall and the Book 4
  chamber. In VR, reach your hand to the Veil and a label above your
  hand offers to open or close it with the trigger; on desktop, press V. It
  opens and closes for everyone in the room. You can pick up the Wand and the
  Sword from the chamber's altar and draw in the air with them, like the pens
  in VRChat.
- **Cabin:** a log cabin by a lake: a fire in a stone hearth, a chess table
  on a hexagram rug with an armchair at each side and a third by the fire, a
  bookshelf, and a big window where the sky holds the real Moon (true phase,
  drawn 2.5× its real size) over the water. In the window-side corner a lantern
  hangs over a magick circle and triangle after Lon Milo DuQuette's "Modern
  Circle" (Book 4's Tau, diamonds and nine lamps, with the names NVIT, HADIT,
  RA HOOR KHVIT, CHAOS, BABALON, LAYLAH and PERDVRABO), scaled down from 9 ft
  to about 1.7 m. A black and a white wand lie on the windowsill for drawing
  in the air, and on desktop V resets the chess board.
- **Balcony:** a balcony 40 storeys up a glass tower, over a big city on an
  overcast, foggy day: two chairs at a little table, a bench, and planters.
  The city is plain boxes painted with window textures; the fog and two
  drifting cloud layers (one hiding the street, one swallowing the tallest
  towers) keep it moody and cheap to draw, and red aircraft lights blink on
  the roofs. Wind plays in the background.
- **Test room:** a plain white room for trying things out, with a chess board
  on its table (see [Chess](#chess)).

## Starting and joining rooms

The first page lists the open rooms: which world, who started it, how long
it's been going, and how many people are in it (a lock means it has a
password). Tap **Join** on one, or **Start a room**:

- **World:** which place to go. New worlds are listed in `public/worlds.json`.
- **Open room:** listed on the first page for anyone to join. An optional
  password keeps it to people you tell; the room is still listed, with a lock.
- **Just me:** the world runs on your device only. Nothing is listed, nothing
  connects to the server after the page and world have downloaded, and the
  mic isn't used.

Then pick a name, an avatar and a color, allow the microphone, and tap
**Enter VR**. **Copy link** in a room copies a link straight to its join
screen (the password isn't in it). Each browser remembers your name, avatar,
color, world, choice of room type and the setting below.

### Sharing a screen

When joining or starting an open room from a computer, **Join as → My screen**
puts what's on that computer into the room instead of you: no avatar or mic,
just a big screen everyone sees, labeled with your name. The browser asks which
screen, window or tab to show, and the next page shows what's going out, lets
you share something else or stop, and picks the sound. Several people can each
share a screen at once.

In the room, anyone can move a screen. In VR, point a hand at it and squeeze
grip; it follows your hand at that distance, upright and turned toward you.
Squeeze the other grip too and move your hands apart or together to resize it.
On desktop, look at it and press **G**, scroll to resize, and **G** again to
put it down. Everyone sees where it is, newcomers included.

Sound depends on the browser doing the sharing:

| Browser | Sound |
|---|---|
| Chrome, Edge, Chromium, Brave | A shared tab's sound (tick **Also share tab audio**). On Windows, sharing the whole screen can include all the computer's sound. |
| Firefox, Safari (any OS) | None from sharing itself, so pick a sound input on the next page instead. |

Any browser can use a sound input. On Linux (PipeWire or PulseAudio), choose
**Monitor of …** for everything the computer plays; in Firefox you can pick it
right in the permission prompt. On Windows, enable **Stereo Mix** (Sound
settings → Recording) or install a virtual cable; on a Mac, a loopback device
like BlackHole. Don't also join the room as yourself from the same computer
while sharing its sound that way, or people hear themselves.

The screen goes out over the same WebRTC connections as voice, so it never
touches the server. The relay carries it when **Hide my IP address** is on,
using about 3 Mbps per viewer at most (1080p, 30 fps), plus 128 kbps stereo
sound. The sharer's upload is that times the number of viewers.

**Hide my IP address** (on by default) sends your voice and movement through
the relay on your server, so others in the room only ever see the server's address.
Turning it off lets your headset connect straight to the others when it can,
with slightly less delay, but then they can see your IP address. It's
per person: someone who turns it off reveals only their own address. The
setting only appears when the server has a relay configured.

A room closes a minute after the last person leaves, so reloading the page
brings you back to the same room. A room nobody enters closes after two
minutes.

| | In VR | On desktop |
|---|---|---|
| Walk | Left stick | WASD / arrows |
| Turn | Right stick (30° snaps) | Q / E, or drag to look |
| Jump | A | Space |
| Sit / stand | X | C |
| Mute | Y or B | M, or the Mic button |
| Pick up / let go (Temple) | Hold grip near the Wand or Sword / release grip | F |
| Draw | Trigger, while holding | Hold the mouse button |
| Erase | Touch a line with an empty hand and pull the trigger | Backspace erases your last line |
| Move a chess piece (Test room) | Hold grip near it, let go over a square | F on it, look at a square, F again |
| Set up the chess board again | Reach for a red button on the table, trigger twice | V |

In VR the Wand or Sword goes into your hand by its hilt, and when you let go
it floats right where it was until someone picks it up again. Lines come from
the tip: gold from the Wand, pale blue from the Sword. Everyone in the room
sees who's holding what and every line, newcomers are caught up, and no one
can take a weapon out of someone else's hand. A room keeps its 300 newest
lines (100,000 points); the oldest go first.

### Chess

The board doesn't know the rules; you move the pieces yourself, like a real
board. The piece your hand would pick up glows, and while you hold one over the
board the square under it lights up, red if there's a piece there already. Let
go and the piece stands in the middle of that square, and the piece that was
there goes off to the side of the board (black's on white's right, white's on
black's). Let go anywhere else and it drops onto the table, or the floor. On
desktop the held piece hovers over wherever you look. Everyone sees the same
board, newcomers included. White is at the end of the table where you arrive.
It's all in `public/js/components/chess.js`; a world adds it with
`<a-entity chess position="…">` on its tabletop. The Test room and the Cabin
each have one; the Cabin builds its table and chairs around wherever the board
is put.

Your left wrist shows whether your mic is on and who's here. Everyone else
sees your avatar follow your head and reach for your hands, with your finger
gestures (point, fist, thumbs-up) from the controller buttons. The mouth moves
when you talk, voices come from where people stand, and a dot under each name
glows while that person speaks.

Sitting puts you on the nearest free log or stump (within about 1.6 m),
facing the fire; each log seats two. Your view settles to seated height
whether you're standing or already sitting at home, worked out from your
avatar's proportions so its hips land on the seat. Others see it sit with its
feet on the ground, leaning when you lean. Walking, jumping, or stepping more
than 0.6 m away in your room stands you up. A jump is a quick hop of about
half a metre, and others see your avatar's knees tuck up.

## How it works

```
Quest browser ──HTTPS──▶ nginx /vr/ ──▶ yesod (Node, 127.0.0.1:8095)
     │                                     • static files
     │                                     • EasyRTC signaling over socket.io
     │                                     • the list of open rooms, room passwords
     │                                     • mints TURN credentials
     └──WebRTC voice, avatar data, shared screens──▶ other headsets directly,
                                      or via coturn (UDP 3478, relay 49160–49999)
```

- **Client:** [A-Frame](https://aframe.io) 1.7.1 with [Networked-A-Frame](https://github.com/networked-aframe/networked-aframe) 0.14.3 (easyrtc adapter, positional audio). Every connection's session description allows Opus stereo at up to 128 kbps (`stereoOpus` in `app.js`), so a shared screen's sound isn't squeezed into mono voice quality; voices use a little more bandwidth as a result.
- **Server:** `server.js`, using Express 4, socket.io and open-easyrtc. It keeps the open rooms in memory (nothing is written to disk), and each EasyRTC connection may only join the room it signed in to, with that room's password. After 10 wrong passwords in 10 minutes, that IP is locked out for the rest of the window. Room passwords ignore case and surrounding spaces.
- **Relay:** coturn with `use-auth-secret`, so each person gets their own TURN username and a credential that lasts 12 hours. It never relays into private or loopback addresses. With **Hide my IP address** on, the browser is set to `iceTransportPolicy: 'relay'`, so it only ever offers the relay's address. Each person then holds one relay allocation (and one UDP port) per other person in the room; `deploy/turnserver.conf` allows 450 in total and 10 per person, which covers the app's 60-person cap.

### Limits

The constants at the top of `server.js` keep a public server from swamping
a small VPS. The first four can be set in the environment
([Settings](#settings)):

| Limit | Value | Why |
|---|---|---|
| People per room | 8 | Everyone streams voice and movement to everyone else, so a headset handles 7 others comfortably |
| Open rooms | 10 | |
| People in rooms, all together | 60 | |
| People from one IP address | 6 | Enough for a household |
| Open rooms started from one IP | 2 | |
| Rooms started per IP per 10 minutes | 6 | |

Measured locally with 56 simulated people in 8 rooms, the server's memory
stayed at about 92 MB, the same as idle: the server only relays the brief
signaling that sets up each connection, so it's the relay (coturn) that grows with use.
Its memory per allocation is small (a few hundred allocations is a few tens of
MB); CPU and bandwidth go up with every person relayed.

### Code map

| File | What it does |
|---|---|
| `server.js` | HTTP, the room list and room API, passwords and limits, EasyRTC auth, ICE/TURN config |
| `public/worlds.json` | The worlds you can pick: name, line of description, thumbnail, fog, and an optional action and shared state |
| `public/js/world.js` | `CurrentWorld()`: finds the loaded world's component (`data-world`), which locomotion and avatars ask for seats, spawn, bounds and floor height |
| `public/index.html` | Lobby and HUD (monospace, centered, `---+---` rules; small inline CSS), the scene, the networked templates, and each world's `<template id="world-…">` |
| `public/js/app.js` | Room list, starting and joining rooms, loading the chosen world, mic/mute, Enter VR, the "who's here" list |
| `public/js/schemas.js` | What NAF syncs for each template; self-hosted hand models |
| `public/js/components/temple.js` | The temple world: the Gnostic Mass hall and the Book 4 chamber, the Veil, and the Wand and Sword |
| `public/js/components/campsite.js` | Builds the world (instanced models, fire, embers, fireflies, pond, jack-o'-lanterns) |
| `public/js/components/ambience.js` | Night sounds: the fire, wind in the trees, owls, twigs and footsteps out in the forest |
| `public/js/components/cabin.js` | The cabin world, built from boxes, cylinders and painted canvas textures, with Nature Kit pines outside |
| `public/js/components/balcony.js` | The balcony world: the city merged into two meshes (concrete and glass facades), the tower's balconies instanced, cloud layers and aircraft lights |
| `public/js/components/sky.js` | The live night sky: stars, Milky Way, Moon and planets, and the moonlight. `pinMoon` holds the Moon at a fixed bearing and altitude (the cabin's window) |
| `public/js/astro.js` | Where the Sun, Moon and planets are (ported from android-toolkit's `:astro`) |
| `public/js/components/vrm-avatar.js` | VRM body that follows the head, two-bone arm IK, lip-sync, blinking |
| `public/js/components/locomotion.js` | Stick/keyboard walking, snap turn, stay-in-the-clearing bounds |
| `public/js/components/hand-tilt.js` | Tips each tracked hand 30° down about the fist (the controller grip pose points too far up), so your hand models, whatever you hold, and the hands others see all angle more naturally. Change it with `hand-tilt="degrees: …"` on `#left-hand` / `#right-hand` |
| `public/js/components/wield.js` | Reaching for a world's `touchables`, picking up its `tools`, holding them by the grip, letting them float, drawing and erasing lines, and keeping all of it in sync (data channels `wield` and `ink`) |
| `public/js/share.js` | "My screen": picking what to share, the sound (from the share or a sound input), sending it to everyone as a stream named `screen`, and its page |
| `public/js/components/screens.js` | Shared screens in the world: showing the sharer's video with its sound coming from the screen, moving and resizing them, and keeping that in sync (data channel `screen`). It reads the sharer's tracks straight off the peer connection, since a screen-only connection carries nothing else |
| `public/js/components/nametag.js`, `voice-level.js`, `player-info.js` | Names, speaking dot, synced player details |

### The sky

The sky is the real sky over the site's location (`SKY_LATITUDE` and
`SKY_LONGITUDE`) at this moment, as it would look from a dark site:
the 9,096 stars of the Yale Bright Star Catalog, the Milky Way, and the Moon
and planets (Mercury to Uranus) where they really are. It always stays night.
The Sun isn't drawn, but it lights the Moon, so the phase is right. A bright
Moon washes out the faint stars and the Milky Way and lights the clearing a
little. Away from the firelight the forest is otherwise nearly black.

True South is toward the pond, the most open view, since from the northern
hemisphere the Moon, the planets and the Milky Way's core all pass through the
southern sky. The trees hide the lowest 20° or so, which at mid-northern
latitudes includes the core.

To look at another time, add `?sky=` to the address, and `&skyspeed=` to run
the clock faster (sky seconds per real second):

```
http://localhost:8095/?sky=2027-07-08T23:30:00-07:00&skyspeed=600
```

The positions are checked against JPL Horizons (they agree to about 0.02°):

```bash
deno test tools/sky/astro_test.js
```

The two data files are built by `tools/sky/` from their sources:
`build-stars.py` packs `bsc5.dat.gz` into `public/assets/sky/stars.bin`, and
`build-milkyway.py` turns NASA's `milkyway_2020_4k.exr` into
`public/assets/sky/milkyway-4k.jpg`. Each script's header has the download URL.

### Sounds

The fire crackles at the fire ring (it fades out by the edge of the
clearing), wind moves through the trees all around, and every 5 to 17 seconds
something happens out among the trees, 14 to 30 m away: a great horned or
barred owl calls (often again, from the same tree), a twig snaps, or footsteps
walk past. They play through the same listener as the voices, so they come
from where they happen. Loudness and frequency are the constants at the top of
`public/js/components/ambience.js`.

The clips in `public/assets/sounds/` (1.2 MB of Ogg Opus) are cut from the
original recordings by `tools/sounds/build-sounds.py`, which lists where to
download each one.

### Adding avatars

Any **VRM 0.x** model with a standard humanoid rig works. Drop `name.vrm` and a
`name-portrait.webp` (192×192, transparent background) into
`public/assets/avatars/`, add `['name', 'Label']` to `AVATARS` in
`public/js/app.js`, then deploy.

The portraits are rendered from the models themselves (head and torso, arms
relaxed, three-quarter view) by `tools/portraits/`:

```bash
deno run --allow-net=127.0.0.1:8097 --allow-read --allow-write=tools/portraits tools/portraits/server.ts
```

Open `http://localhost:8097/?slugs=name` in a browser and wait for the tab
title to say "done". Then trim and size the PNG from `tools/portraits/out/`:

```bash
magick tools/portraits/out/name.png -trim +repage -resize 168x168 -gravity south -background none -extent 192x192 -quality 88 public/assets/avatars/name-portrait.webp
``` Avatars are
scaled to a common height automatically. Keep them low-poly (under about 10k
triangles) for the Quest. The [open-source-avatars](https://github.com/ToxSam/open-source-avatars)
registry lists many more CC0 VRMs, including the rest of the 100Avatars series.

### Adding a world

1. Add an entry to `public/worlds.json` (`id`, `name`, `blurb`, `thumbnail`,
   and optionally `fog`). The server only accepts rooms for worlds listed there.
2. Add a `<template id="world-<id>">` to `public/index.html` with the world's
   entities (lights, sky, models). It's cloned into the scene when someone
   picks that world, so its assets only download then. Mark the entity that
   holds the world's main component with `data-world="<component>"`, and load
   the component's script in the page's `<head>`.
3. Add a 640×360 `public/assets/worlds/<id>.webp` for the picker and room list.

The marked component tells locomotion and the avatars about the place (see
`public/js/world.js`):

| | |
|---|---|
| `seats` | `[{ x, y, z, yaw }]`, where X / C sits you (required, can be empty) |
| `spawn` | `{ x, z, yaw }` where people arrive. Without it, people arrive on a circle around the origin, facing in |
| `confine(headPos)` | moves a head position back inside the walkable area. Without it, people stay within 24 m of the origin and 0.9 m from it (the campfire) |
| `floorAt(x, z)` | floor height there; people ease up and down steps. Without it, the floor is flat at 0 |
| `screen` | `{ x, y, z, yaw, width }`: where a shared screen first appears (the middle of the picture; at yaw 0 it faces +z). Without it, 4 m ahead of `spawn` at 1.8 m |
| `touchables` | `[{ name, label(), distance(worldPos), use() }]`: things to reach for in VR (`wield.js`). Within 20 cm of an empty hand, a label above the hand reads "Trigger: " + `label()`, and the trigger calls `use()` |
| `tools` | `[{ name, label, object, grip, tip, butt, ink }]`: things to pick up and draw with (`wield.js`). Each `object` lies along its own +x; `grip`, `tip` and `butt` are local `Vector3`s and `ink` is a `#rrggbb` color |

Two optional fields in `worlds.json` cover interaction:

- `action: { label, method }`: V on desktop calls that method on the
  component, and the controls hint shows the label. In VR the world offers the
  same thing as a `touchable` that you reach for (below).
- `shared: [property, …]`: those component properties stay the same for
  everyone in the room. Changes go to everyone, and people already there tell
  each newcomer how things stand. Values from others are only accepted for
  the listed properties and with the type they already have.

The temple uses all of these (`action` and its one touchable toggle the
Veil, `shared` is `["veil"]`, and its `tools` are the Wand and the Sword).

## Running it locally

```bash
npm install
```

```bash
node server.js
```

Deno also runs it (`deno install`, then
`deno run --allow-net --allow-read --allow-env --allow-sys server.js`).

Then open http://localhost:8095/ in two tabs, start an open room in one and
join it from the other. Without a relay configured, **Hide my IP address**
doesn't appear and people connect directly. Headsets need HTTPS for WebXR
and the microphone, so to try it on a Quest, put it behind a server with a
certificate (below) or a tunnel.

## Settings

The server reads these from the environment (`deploy/env.example` lists
them all). Every one is optional.

| Variable | Default | What it does |
|---|---|---|
| `HOST`, `PORT` | `127.0.0.1`, `8095` | Where the app listens |
| `TURN_SECRET`, `TURN_URLS` | none | The coturn relay; needed for **Hide my IP address** |
| `STUN_URLS` | none | STUN servers, comma-separated |
| `SITE_NAME` | `Yesod` | What the lobby and the browser tab call the site |
| `SKY_LATITUDE`, `SKY_LONGITUDE` | Greenwich | Where the worlds' sky is seen from, in degrees (north and east positive) |
| `MAX_PEOPLE`, `MAX_ROOMS`, `MAX_CONNECTIONS`, `MAX_CONNECTIONS_PER_IP` | 8, 10, 60, 6 | The limits above |
| `EASYRTC_LOG_LEVEL` | `error` | EasyRTC's logging |

## Installing on a server

These steps are for Debian or Ubuntu with nginx already serving your domain
over HTTPS. Replace `example.org` with your domain throughout. Run them as root.

1. Install Node.js and coturn, and add a user for the app:

   ```bash
   apt install nodejs npm coturn
   ```

   ```bash
   useradd --system --no-create-home --shell /usr/sbin/nologin yesod
   ```

2. Copy `deploy/env.example` to `/etc/yesod/env` on the server. Fill in your
   domain and a new secret (`openssl rand -hex 32`), and set your location for
   the sky. Then let only root and the app read it:

   ```bash
   chown root:yesod /etc/yesod/env && chmod 640 /etc/yesod/env
   ```

3. Set up coturn: copy `deploy/turnserver.conf` to `/etc/turnserver.conf`
   (keep Debian's original if you like), set `realm` to your domain and
   `static-auth-secret` to the same secret as `TURN_SECRET`. On Debian, also set
   `TURNSERVER_ENABLED=1` in `/etc/default/coturn` if it's there. Then
   `systemctl restart coturn`.

4. Open the relay's ports. With ufw:

   ```bash
   ufw allow 3478 comment yesod && ufw allow 49160:49999/udp comment yesod
   ```

5. Copy `deploy/yesod.service` to `/etc/systemd/system/` on the server, then:

   ```bash
   systemctl daemon-reload && systemctl enable yesod
   ```

6. Add nginx's part: copy `deploy/nginx-yesod.conf` to
   `/etc/nginx/snippets/yesod.conf`, add `include snippets/yesod.conf;` to
   your site's `server { listen 443 … }` block, then
   `nginx -t && systemctl reload nginx`. It serves the app at `/vr/`; change
   both `location` lines for another path.

7. From your computer, copy `deploy/deploy.env.example` to
   `deploy/deploy.env`, set `DEPLOY_TARGET`, and run `deploy/deploy.sh`.

The service runs sandboxed with a 300 MB memory cap; the app itself uses
about 30 MB. On a small VPS the relay's bandwidth is the real limit (see
[Limits](#limits)).

### Updating

```bash
deploy/deploy.sh
```

This uploads the app, runs `npm ci` on the server, swaps in the new version
(the previous one is kept as `/opt/yesod.old`) and restarts the service.
The paths and the service name can be changed in `deploy/deploy.env`.

```bash
journalctl -u yesod -f
```

### What it remembers about visitors

Nothing is logged about who comes by or when:

- The nginx snippet keeps no access log for the app, and logs only critical
  errors there.
- coturn's log goes to `/dev/null`. To debug the relay, replace `no-stdout-log`,
  `log-file` and `simple-log` with `syslog` and `verbose` for a while.
- The app logs only startup and its own errors. Rooms, wrong passwords and
  per-IP counts live in memory only, are never written down, and are gone on
  restart. The room list shows only the world, the starter's name, how long
  it's been open and how many are in it. EasyRTC logs errors only.

Voice and movement are encrypted end to end (WebRTC). With **Hide my IP
address** on (the default), they go through the relay, which carries them
without being able to hear or read them, and others in the room only see the
server's address. With it off, people connect directly when they can, and can
see each other's IP addresses (in open rooms, that includes strangers).

### Changing the TURN secret

The relay's credentials are minted from `TURN_SECRET`, which must match
`static-auth-secret` in `/etc/turnserver.conf`. To replace it (as root):

```bash
new=$(openssl rand -hex 32); sed -i "s|^TURN_SECRET=.*|TURN_SECRET=$new|" /etc/yesod/env; sed -i "s|^static-auth-secret=.*|static-auth-secret=$new|" /etc/turnserver.conf; unset new; systemctl restart coturn yesod
```

### Removing it

Stop and disable `yesod` and `coturn`, delete the `include` line from the
nginx site and reload nginx, remove the ufw rules, then delete `/opt/yesod`,
`/etc/yesod` and `/etc/systemd/system/yesod.service`.

## Credits

All third-party assets are free to use, and every one is self-hosted.

- **Stele of Revealing photo** (temple): a replica photographed by A.M.A., public domain, via [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Stelae_front.jpg) (`public/assets/temple/CREDITS.txt`).
- **Avatars:** [100Avatars](https://www.polygonalmind.com/100-avatars) by Polygonal Mind, CC0.
- **Chess pieces:** [Stylized Chess Pieces](https://opengameart.org/content/stylized-chess-pieces) by Lyricsz, CC0 (`public/assets/chess/CREDITS.txt`).
- **World models:** [Nature Kit](https://kenney.nl/assets/nature-kit) by Kenney, CC0 (`public/assets/env/LICENSE-kenney-nature-kit.txt`).
- **Milky Way:** [Deep Star Maps 2020](https://svs.gsfc.nasa.gov/4851), NASA/Goddard Space Flight Center Scientific Visualization Studio; Gaia DR2: ESA/Gaia/DPAC.
- **Stars:** [Yale Bright Star Catalog](http://tdc-www.harvard.edu/catalogs/bsc5.html), 5th revised edition (Hoffleit and Warren, 1991).
- **Planet positions:** Paul Schlyter, [How to compute planetary positions](https://stjarnhimlen.se/comp/ppcomp.html).
- **Sounds:** fire, wind, twigs, footsteps and the chess pieces' knocks (a gavel, dice on a table) from [BigSoundBank](https://bigsoundbank.com) by Joseph Sardin, CC0. Barred owl: National Park Service (Yellowstone), public domain, via [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Barred_Owl,_Yellowstone_National_Park.oga). Great horned owl: [XC450919](https://commons.wikimedia.org/wiki/File:Bubo_virginianus_-_Great_Horned_Owl_XC450919.mp3) by Michael & Katie LaTour (xeno-canto), [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/); the trimmed, filtered clips `owl-great-horned-*.ogg` are shared under the same license.
- **Hand models:** [aframevr/assets](https://github.com/aframevr/assets), MIT.
- **Font:** Roboto MSDF from A-Frame, Apache 2.0.
- **Code:** A-Frame (MIT), Networked-A-Frame (MIT), open-easyrtc (BSD-2-Clause), socket.io (MIT), Express (MIT).