weed-cli 2.4.4__tar.gz → 2.4.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {weed_cli-2.4.4/weed_cli.egg-info → weed_cli-2.4.5}/PKG-INFO +64 -16
- {weed_cli-2.4.4 → weed_cli-2.4.5}/README.md +63 -15
- {weed_cli-2.4.4 → weed_cli-2.4.5}/pyproject.toml +1 -1
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tests/test_web_ui_api.py +47 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/web_ui.py +72 -4
- {weed_cli-2.4.4 → weed_cli-2.4.5/weed_cli.egg-info}/PKG-INFO +64 -16
- {weed_cli-2.4.4 → weed_cli-2.4.5}/LICENSE +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/dht.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/discovery_relay.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/lightning_settle.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/node.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/poc_reputation.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/setup.cfg +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/shell.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tests/test_dht.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tests/test_discovery_relay.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tests/test_host_live_reload.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tests/test_node_manifest.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tests/test_orbit_ws.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tests/test_timing_challenge.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tests/test_tunnel_failover.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tests/testutil.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/tunnel_relay.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/weed.py +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/weed_cli.egg-info/SOURCES.txt +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/weed_cli.egg-info/dependency_links.txt +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/weed_cli.egg-info/entry_points.txt +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/weed_cli.egg-info/requires.txt +0 -0
- {weed_cli-2.4.4 → weed_cli-2.4.5}/weed_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.5
|
|
4
4
|
Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
|
|
@@ -55,6 +55,8 @@ output (not simulated) — see [Status](#status) for what's verified.
|
|
|
55
55
|
- [CLI](#cli)
|
|
56
56
|
- [Interactive shell](#interactive-shell)
|
|
57
57
|
- [Web UI](#web-ui)
|
|
58
|
+
- [Orbit Visualizer](#orbit-visualizer)
|
|
59
|
+
- [Orbit Visualizer plugins](#orbit-visualizer-plugins)
|
|
58
60
|
- [Docker](#docker)
|
|
59
61
|
- [Core mechanisms](#core-mechanisms)
|
|
60
62
|
- [The integrated node](#the-integrated-node)
|
|
@@ -185,9 +187,19 @@ browser opened with that token gets the **party view** and nothing
|
|
|
185
187
|
else — the live Orbit stream, what's playing, a vote on which
|
|
186
188
|
downloaded track plays next (one vote per person per track, toggleable),
|
|
187
189
|
and whatever links you set. The admin's **Party** tab holds the guest
|
|
188
|
-
link and its QR code, the title and links, the live tally with a ▶
|
|
189
|
-
track, and an "auto-play the top vote when a
|
|
190
|
+
link and its QR code, the title and links, the live tally with a ▶ and
|
|
191
|
+
a ♫+ (add to playlist) per track, and an "auto-play the top vote when a
|
|
192
|
+
track ends" switch. The guest page itself is bare: the live picture and
|
|
193
|
+
"now playing" pinned to the top, the vote list scrolling beneath —
|
|
194
|
+
alphabetical, with an iPod-style A–Z strip down the right edge to jump
|
|
195
|
+
by letter, and a "leading:" line for the current front-runners.
|
|
190
196
|
Guests can't host, download, like, subscribe, or read the library.
|
|
197
|
+
Switch on **guest chat** in the Party tab and everyone gets a message
|
|
198
|
+
box: the last few messages are drawn over the picture — into the
|
|
199
|
+
visualizer's canvas, so the stream carries them to VLC, a Roku and the
|
|
200
|
+
guest page's picture alike (the visualizer's 💬 toggles that), and as
|
|
201
|
+
an overlay on the plain player window. Messages live in memory only
|
|
202
|
+
(the last 200), one per second per person, 200 characters each.
|
|
191
203
|
|
|
192
204
|
`--stream-plain-port` adds a plain-HTTP listener that serves *only* the
|
|
193
205
|
stream endpoints (the Orbit MJPEG stream and downloaded files), for
|
|
@@ -201,17 +213,44 @@ Includes real HTTP range support (`/api/stream/<job_id>`) so a
|
|
|
201
213
|
`<video>` tag can seek a completed download instead of downloading it
|
|
202
214
|
blind.
|
|
203
215
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
### Orbit Visualizer
|
|
217
|
+
|
|
218
|
+
The player's 🌀 button opens the **Orbit Visualizer**: a canvas driven by
|
|
219
|
+
the track's audio (Web Audio analyser) and a low-res sample of its
|
|
220
|
+
picture. Eleven built-in modes — Tunnel, Bars, Mirror, Scope, Spiral,
|
|
221
|
+
Pixels, ASCII (six character sets, natural or neon), Plasma, Kaleido,
|
|
222
|
+
Particles, Freefall — plus the ten in `web/orbit_extras.js` (next
|
|
223
|
+
section). Click the lit mode again for the plain video. Speed, React and
|
|
224
|
+
Zoom retune every mode; every setting persists in the browser.
|
|
225
|
+
|
|
226
|
+
- **Fade**: the transition between modes and between tracks — Burn,
|
|
227
|
+
Warp, Glitch, Pixelate, Crossfade, Wipe, the plugin ones, or Random
|
|
228
|
+
(the ⚄ button picks which ones Random may draw from), with a length
|
|
229
|
+
slider. A transition can declare its own length multiple (Win95's
|
|
230
|
+
crash runs at 3×).
|
|
231
|
+
- **Navigation**, Blender-style: scroll zooms, left-drag pans,
|
|
232
|
+
middle-drag rotates, shift+middle pans, ctrl+middle zooms,
|
|
233
|
+
double-click resets. `f` fullscreen, ←/→ cycle modes (fullscreen),
|
|
234
|
+
shift+1-9,0 jump to a mode, ↑/↓ ASCII brightness, `[`/`]` resolution.
|
|
235
|
+
- **Network stream** (📡): the canvas goes out as MJPEG on
|
|
236
|
+
`/api/orbit-view` for VLC (`vlc --demux=mjpeg --network-caching=300
|
|
237
|
+
<url>`; 🔗 copies the URL, token included), a Roku IP-camera viewer,
|
|
238
|
+
or the guest party page. Resolution and JPEG quality are selectable;
|
|
239
|
+
the ⏱ slider delays the local audio by up to 10 s to line up with a
|
|
240
|
+
laggy viewer. The stream keeps running while the visualizer is closed,
|
|
241
|
+
minimised to PIP, or in a background tab. Picture only — audio stays
|
|
242
|
+
wherever the browser is playing it.
|
|
243
|
+
- **MIDI** (🎹): an AKAI MPK mini or any Web MIDI controller
|
|
244
|
+
(Chrome/Edge/Firefox). Pads pick modes and fire actions, knobs turn
|
|
245
|
+
the sliders or sweep through modes/transitions/character sets;
|
|
246
|
+
endless encoders are detected on their own. Every row, plugin modes
|
|
247
|
+
and transitions included, has a Learn button; **export** saves the
|
|
248
|
+
keymap as a `.json` file and **import** loads one.
|
|
249
|
+
- **Video swap** (⇄ on the player): another download's picture stands
|
|
250
|
+
in for the current track's — for an mp3 or a static-image video, so
|
|
251
|
+
the visuals have real footage. Audio stays with the track; the
|
|
252
|
+
borrowed video loops on its own, follows play/pause, and the pairing
|
|
253
|
+
is remembered per track.
|
|
215
254
|
|
|
216
255
|
### Orbit Visualizer plugins
|
|
217
256
|
|
|
@@ -444,8 +483,13 @@ just designed:
|
|
|
444
483
|
- Transitive trust through the subscribe graph
|
|
445
484
|
- NAT traversal via relay-mediated tunneling, with TLS and heartbeat
|
|
446
485
|
- Kademlia DHT discovery, survives the announcing node going offline
|
|
447
|
-
- Local web UI with live progress, QR onboarding,
|
|
448
|
-
-
|
|
486
|
+
- Local web UI with live progress, QR onboarding, HTTP range streaming,
|
|
487
|
+
optional two-tier token auth, and the guest party page with voting
|
|
488
|
+
- Orbit Visualizer: 21 modes and 16 transitions behind a plugin API,
|
|
489
|
+
MJPEG network stream (VLC/Roku), Web MIDI control with keymap files,
|
|
490
|
+
video swap — 44 Playwright tests against real Chromium (`tests/e2e/`)
|
|
491
|
+
- Containerized node (`Dockerfile.node`, `docker-compose.node.yml`),
|
|
492
|
+
with `web/` bind-mounted so frontend edits need no rebuild
|
|
449
493
|
|
|
450
494
|
## Known limitations
|
|
451
495
|
|
|
@@ -484,3 +528,7 @@ mechanisms hold up:
|
|
|
484
528
|
and votes are anonymous per browser cookie — enough to put a
|
|
485
529
|
LAN-bound UI behind something and hand guests a safe subset, not a
|
|
486
530
|
reason to face it at the internet.
|
|
531
|
+
- The Orbit stream is picture only, as MJPEG: fine for VLC, an
|
|
532
|
+
IP-camera app or the party page, but no audio travels with it and
|
|
533
|
+
there's no HLS/MP4 endpoint, so a stock Roku or smart-TV player
|
|
534
|
+
can't take it directly.
|
|
@@ -23,6 +23,8 @@ output (not simulated) — see [Status](#status) for what's verified.
|
|
|
23
23
|
- [CLI](#cli)
|
|
24
24
|
- [Interactive shell](#interactive-shell)
|
|
25
25
|
- [Web UI](#web-ui)
|
|
26
|
+
- [Orbit Visualizer](#orbit-visualizer)
|
|
27
|
+
- [Orbit Visualizer plugins](#orbit-visualizer-plugins)
|
|
26
28
|
- [Docker](#docker)
|
|
27
29
|
- [Core mechanisms](#core-mechanisms)
|
|
28
30
|
- [The integrated node](#the-integrated-node)
|
|
@@ -153,9 +155,19 @@ browser opened with that token gets the **party view** and nothing
|
|
|
153
155
|
else — the live Orbit stream, what's playing, a vote on which
|
|
154
156
|
downloaded track plays next (one vote per person per track, toggleable),
|
|
155
157
|
and whatever links you set. The admin's **Party** tab holds the guest
|
|
156
|
-
link and its QR code, the title and links, the live tally with a ▶
|
|
157
|
-
track, and an "auto-play the top vote when a
|
|
158
|
+
link and its QR code, the title and links, the live tally with a ▶ and
|
|
159
|
+
a ♫+ (add to playlist) per track, and an "auto-play the top vote when a
|
|
160
|
+
track ends" switch. The guest page itself is bare: the live picture and
|
|
161
|
+
"now playing" pinned to the top, the vote list scrolling beneath —
|
|
162
|
+
alphabetical, with an iPod-style A–Z strip down the right edge to jump
|
|
163
|
+
by letter, and a "leading:" line for the current front-runners.
|
|
158
164
|
Guests can't host, download, like, subscribe, or read the library.
|
|
165
|
+
Switch on **guest chat** in the Party tab and everyone gets a message
|
|
166
|
+
box: the last few messages are drawn over the picture — into the
|
|
167
|
+
visualizer's canvas, so the stream carries them to VLC, a Roku and the
|
|
168
|
+
guest page's picture alike (the visualizer's 💬 toggles that), and as
|
|
169
|
+
an overlay on the plain player window. Messages live in memory only
|
|
170
|
+
(the last 200), one per second per person, 200 characters each.
|
|
159
171
|
|
|
160
172
|
`--stream-plain-port` adds a plain-HTTP listener that serves *only* the
|
|
161
173
|
stream endpoints (the Orbit MJPEG stream and downloaded files), for
|
|
@@ -169,17 +181,44 @@ Includes real HTTP range support (`/api/stream/<job_id>`) so a
|
|
|
169
181
|
`<video>` tag can seek a completed download instead of downloading it
|
|
170
182
|
blind.
|
|
171
183
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
### Orbit Visualizer
|
|
185
|
+
|
|
186
|
+
The player's 🌀 button opens the **Orbit Visualizer**: a canvas driven by
|
|
187
|
+
the track's audio (Web Audio analyser) and a low-res sample of its
|
|
188
|
+
picture. Eleven built-in modes — Tunnel, Bars, Mirror, Scope, Spiral,
|
|
189
|
+
Pixels, ASCII (six character sets, natural or neon), Plasma, Kaleido,
|
|
190
|
+
Particles, Freefall — plus the ten in `web/orbit_extras.js` (next
|
|
191
|
+
section). Click the lit mode again for the plain video. Speed, React and
|
|
192
|
+
Zoom retune every mode; every setting persists in the browser.
|
|
193
|
+
|
|
194
|
+
- **Fade**: the transition between modes and between tracks — Burn,
|
|
195
|
+
Warp, Glitch, Pixelate, Crossfade, Wipe, the plugin ones, or Random
|
|
196
|
+
(the ⚄ button picks which ones Random may draw from), with a length
|
|
197
|
+
slider. A transition can declare its own length multiple (Win95's
|
|
198
|
+
crash runs at 3×).
|
|
199
|
+
- **Navigation**, Blender-style: scroll zooms, left-drag pans,
|
|
200
|
+
middle-drag rotates, shift+middle pans, ctrl+middle zooms,
|
|
201
|
+
double-click resets. `f` fullscreen, ←/→ cycle modes (fullscreen),
|
|
202
|
+
shift+1-9,0 jump to a mode, ↑/↓ ASCII brightness, `[`/`]` resolution.
|
|
203
|
+
- **Network stream** (📡): the canvas goes out as MJPEG on
|
|
204
|
+
`/api/orbit-view` for VLC (`vlc --demux=mjpeg --network-caching=300
|
|
205
|
+
<url>`; 🔗 copies the URL, token included), a Roku IP-camera viewer,
|
|
206
|
+
or the guest party page. Resolution and JPEG quality are selectable;
|
|
207
|
+
the ⏱ slider delays the local audio by up to 10 s to line up with a
|
|
208
|
+
laggy viewer. The stream keeps running while the visualizer is closed,
|
|
209
|
+
minimised to PIP, or in a background tab. Picture only — audio stays
|
|
210
|
+
wherever the browser is playing it.
|
|
211
|
+
- **MIDI** (🎹): an AKAI MPK mini or any Web MIDI controller
|
|
212
|
+
(Chrome/Edge/Firefox). Pads pick modes and fire actions, knobs turn
|
|
213
|
+
the sliders or sweep through modes/transitions/character sets;
|
|
214
|
+
endless encoders are detected on their own. Every row, plugin modes
|
|
215
|
+
and transitions included, has a Learn button; **export** saves the
|
|
216
|
+
keymap as a `.json` file and **import** loads one.
|
|
217
|
+
- **Video swap** (⇄ on the player): another download's picture stands
|
|
218
|
+
in for the current track's — for an mp3 or a static-image video, so
|
|
219
|
+
the visuals have real footage. Audio stays with the track; the
|
|
220
|
+
borrowed video loops on its own, follows play/pause, and the pairing
|
|
221
|
+
is remembered per track.
|
|
183
222
|
|
|
184
223
|
### Orbit Visualizer plugins
|
|
185
224
|
|
|
@@ -412,8 +451,13 @@ just designed:
|
|
|
412
451
|
- Transitive trust through the subscribe graph
|
|
413
452
|
- NAT traversal via relay-mediated tunneling, with TLS and heartbeat
|
|
414
453
|
- Kademlia DHT discovery, survives the announcing node going offline
|
|
415
|
-
- Local web UI with live progress, QR onboarding,
|
|
416
|
-
-
|
|
454
|
+
- Local web UI with live progress, QR onboarding, HTTP range streaming,
|
|
455
|
+
optional two-tier token auth, and the guest party page with voting
|
|
456
|
+
- Orbit Visualizer: 21 modes and 16 transitions behind a plugin API,
|
|
457
|
+
MJPEG network stream (VLC/Roku), Web MIDI control with keymap files,
|
|
458
|
+
video swap — 44 Playwright tests against real Chromium (`tests/e2e/`)
|
|
459
|
+
- Containerized node (`Dockerfile.node`, `docker-compose.node.yml`),
|
|
460
|
+
with `web/` bind-mounted so frontend edits need no rebuild
|
|
417
461
|
|
|
418
462
|
## Known limitations
|
|
419
463
|
|
|
@@ -452,3 +496,7 @@ mechanisms hold up:
|
|
|
452
496
|
and votes are anonymous per browser cookie — enough to put a
|
|
453
497
|
LAN-bound UI behind something and hand guests a safe subset, not a
|
|
454
498
|
reason to face it at the internet.
|
|
499
|
+
- The Orbit stream is picture only, as MJPEG: fine for VLC, an
|
|
500
|
+
IP-camera app or the party page, but no audio travels with it and
|
|
501
|
+
there's no HLS/MP4 endpoint, so a stock Roku or smart-TV player
|
|
502
|
+
can't take it directly.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weed-cli"
|
|
7
|
-
version = "2.4.
|
|
7
|
+
version = "2.4.5"
|
|
8
8
|
description = "Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -660,3 +660,50 @@ def test_request_with_no_origin_header_is_allowed(web_server):
|
|
|
660
660
|
through (see web_ui.py's _check_origin docstring)."""
|
|
661
661
|
status, resp = http_post_json(f'{web_server}/api/like', {'content_hash': 'd' * 64})
|
|
662
662
|
assert status == 200
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def test_party_chat_is_gated_rate_limited_and_polled_by_id(web_server, monkeypatch):
|
|
666
|
+
"""The party chat: off until the admin turns it on, one message a
|
|
667
|
+
second per person, 200 characters, guests and admin both post, the
|
|
668
|
+
ring is polled by id, and only the admin can wipe it."""
|
|
669
|
+
import web_ui
|
|
670
|
+
monkeypatch.setattr(web_ui, 'AUTH_TOKEN', 'admin-secret')
|
|
671
|
+
monkeypatch.setattr(web_ui, 'STREAM_TOKEN', 'guest-secret')
|
|
672
|
+
monkeypatch.setattr(web_ui, 'CHAT_MIN_INTERVAL', 0.3)
|
|
673
|
+
with web_ui._chat_lock:
|
|
674
|
+
web_ui._chat_messages.clear(); web_ui._chat_last_post.clear()
|
|
675
|
+
guest = {'Authorization': 'Bearer guest-secret'}
|
|
676
|
+
admin = {'Authorization': 'Bearer admin-secret'}
|
|
677
|
+
|
|
678
|
+
# off by default: readable, but nobody can post
|
|
679
|
+
status, headers, body = _raw_get(f'{web_server}/api/chat', guest)
|
|
680
|
+
assert status == 200 and json.loads(body) == {'enabled': False, 'messages': [], 'you': json.loads(body)['you']}
|
|
681
|
+
assert json.loads(body)['you'].startswith('guest-')
|
|
682
|
+
assert _raw_post(f'{web_server}/api/chat', {'text': 'hi'}, guest)[0] == 403
|
|
683
|
+
assert _raw_post(f'{web_server}/api/chat', {'text': 'hi'}, admin)[0] == 403
|
|
684
|
+
assert _raw_post(f'{web_server}/api/chat', {'text': 'hi'})[0] == 401 # no token at all
|
|
685
|
+
|
|
686
|
+
status, _, r = _raw_post(f'{web_server}/api/party/config', {'chat': True}, admin)
|
|
687
|
+
assert status == 200 and r['party']['chat'] is True
|
|
688
|
+
assert json.loads(_raw_get(f'{web_server}/api/party', guest)[2])['chat'] is True
|
|
689
|
+
|
|
690
|
+
voter = {**guest, 'Cookie': headers['set-cookie'].split(';')[0]}
|
|
691
|
+
status, _, r = _raw_post(f'{web_server}/api/chat', {'text': ' turn it up ', 'name': 'dave'}, voter)
|
|
692
|
+
assert status == 200 and r['message']['name'] == 'dave' and r['message']['text'] == 'turn it up' and r['message']['role'] == 'guest'
|
|
693
|
+
first_id = r['message']['id']
|
|
694
|
+
assert _raw_post(f'{web_server}/api/chat', {'text': 'again'}, voter)[0] == 429 # too fast
|
|
695
|
+
assert _raw_post(f'{web_server}/api/chat', {'text': ' '}, admin)[0] == 400
|
|
696
|
+
assert _raw_post(f'{web_server}/api/chat', {'text': 'x' * 201}, admin)[0] == 400
|
|
697
|
+
status, _, r = _raw_post(f'{web_server}/api/chat', {'text': 'no'}, admin)
|
|
698
|
+
assert status == 200 and r['message']['name'] == 'host' and r['message']['role'] == 'admin'
|
|
699
|
+
|
|
700
|
+
_, _, body = _raw_get(f'{web_server}/api/chat', voter)
|
|
701
|
+
msgs = json.loads(body)['messages']
|
|
702
|
+
assert [m['text'] for m in msgs] == ['turn it up', 'no']
|
|
703
|
+
_, _, body = _raw_get(f'{web_server}/api/chat?since={first_id}', voter)
|
|
704
|
+
assert [m['text'] for m in json.loads(body)['messages']] == ['no']
|
|
705
|
+
|
|
706
|
+
assert _raw_post(f'{web_server}/api/chat/clear', {}, voter)[0] == 401
|
|
707
|
+
status, _, r = _raw_post(f'{web_server}/api/chat/clear', {}, admin)
|
|
708
|
+
assert status == 200 and r['cleared'] == 2
|
|
709
|
+
assert json.loads(_raw_get(f'{web_server}/api/chat', voter)[2])['messages'] == []
|
|
@@ -77,13 +77,24 @@ AUTH_COOKIE_MAX_AGE = 30 * 24 * 3600
|
|
|
77
77
|
# track rather than one per click; nothing else is keyed on it
|
|
78
78
|
GUEST_COOKIE = 'weed_guest'
|
|
79
79
|
_party_votes = {} # content_hash -> set of guest ids (in memory; a restart clears the tally)
|
|
80
|
+
# the party chat: a short in-memory ring of {id, name, text, ts, role}.
|
|
81
|
+
# Guests and the admin post; the visualizer draws the last few over the
|
|
82
|
+
# picture (so the stream carries them) and the guest page lists them.
|
|
83
|
+
# Off unless the admin turns it on in the Party tab (party['chat']).
|
|
84
|
+
CHAT_KEEP = 200
|
|
85
|
+
CHAT_MAX_TEXT = 200
|
|
86
|
+
CHAT_MIN_INTERVAL = 1.0 # seconds between posts, per person
|
|
87
|
+
_chat_messages = []
|
|
88
|
+
_chat_seq = 0
|
|
89
|
+
_chat_last_post = {} # guest id (or 'admin') -> last post time
|
|
90
|
+
_chat_lock = threading.Lock()
|
|
80
91
|
|
|
81
92
|
|
|
82
93
|
def _party_settings():
|
|
83
94
|
"""Caller must hold _lock. The library's 'party' dict, created on
|
|
84
95
|
demand -- a library written (or a test fixture built) before this
|
|
85
96
|
key existed simply doesn't have it."""
|
|
86
|
-
return _library.setdefault('party', {'title': '', 'links': [], 'autoplay': False})
|
|
97
|
+
return _library.setdefault('party', {'title': '', 'links': [], 'autoplay': False, 'chat': False})
|
|
87
98
|
_orbit_ws_since = 0.0 # when the current stream started -- the party page keys its <img> on it
|
|
88
99
|
# Optional second listener, plain HTTP, serving *only* the stream
|
|
89
100
|
# endpoints (/api/orbit-view, /api/orbit-stream, /api/stream/<job>).
|
|
@@ -863,7 +874,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
863
874
|
|
|
864
875
|
# GET endpoints a guest (stream token) may use; everything else under
|
|
865
876
|
# /api/ is admin-only. The stream endpoints also take ?token=.
|
|
866
|
-
_GUEST_GET = ('/api/orbit-view', '/api/orbit-stream', '/api/config', '/api/party')
|
|
877
|
+
_GUEST_GET = ('/api/orbit-view', '/api/orbit-stream', '/api/config', '/api/party', '/api/chat')
|
|
867
878
|
|
|
868
879
|
def do_GET(self):
|
|
869
880
|
parsed = urlparse(self.path)
|
|
@@ -910,6 +921,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
910
921
|
return self._json(cfg)
|
|
911
922
|
if path == '/api/party':
|
|
912
923
|
return self._handle_party()
|
|
924
|
+
if path == '/api/chat':
|
|
925
|
+
return self._handle_chat_get(qs)
|
|
913
926
|
if path == '/api/discover':
|
|
914
927
|
results = node.group_discover_by_content(node.discover(qs.get('relay') or [DEFAULT_RELAY]))
|
|
915
928
|
return self._json({'results': results})
|
|
@@ -998,7 +1011,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
998
1011
|
# /api/login is how a browser *gets* the cookie (the unlock
|
|
999
1012
|
# prompt); voting is the one thing a guest may POST; everything
|
|
1000
1013
|
# else needs the admin token already
|
|
1001
|
-
if path
|
|
1014
|
+
if path in ('/api/party/vote', '/api/chat'):
|
|
1002
1015
|
if not self._guest_ok():
|
|
1003
1016
|
return self._deny()
|
|
1004
1017
|
elif path != '/api/login' and not self._authorized():
|
|
@@ -1036,6 +1049,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1036
1049
|
handlers = {
|
|
1037
1050
|
'/api/party/vote': self._handle_party_vote, '/api/party/config': self._handle_party_config,
|
|
1038
1051
|
'/api/party/played': self._handle_party_played,
|
|
1052
|
+
'/api/chat': self._handle_chat_post, '/api/chat/clear': self._handle_chat_clear,
|
|
1039
1053
|
'/api/host': self._handle_host, '/api/host/forget': self._handle_forget_host,
|
|
1040
1054
|
'/api/download': self._handle_download,
|
|
1041
1055
|
'/api/like': self._handle_like, '/api/subscribe': self._handle_subscribe,
|
|
@@ -1339,7 +1353,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1339
1353
|
plain = self._stream_plain_base()
|
|
1340
1354
|
self._json({
|
|
1341
1355
|
'role': self._role(), 'title': party.get('title') or '', 'links': party.get('links') or [],
|
|
1342
|
-
'autoplay': bool(party.get('autoplay')),
|
|
1356
|
+
'autoplay': bool(party.get('autoplay')), 'chat': bool(party.get('chat')),
|
|
1343
1357
|
'now_playing': now_playing,
|
|
1344
1358
|
'stream': {
|
|
1345
1359
|
'active': active, 'since': _orbit_ws_since,
|
|
@@ -1386,9 +1400,63 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1386
1400
|
party['links'] = links[:20]
|
|
1387
1401
|
if 'autoplay' in body:
|
|
1388
1402
|
party['autoplay'] = bool(body.get('autoplay'))
|
|
1403
|
+
if 'chat' in body:
|
|
1404
|
+
party['chat'] = bool(body.get('chat'))
|
|
1389
1405
|
_save_library()
|
|
1390
1406
|
self._json({'ok': True, 'party': dict(party)})
|
|
1391
1407
|
|
|
1408
|
+
# ── party chat ──────────────────────────────────────────────────
|
|
1409
|
+
def _handle_chat_get(self, qs):
|
|
1410
|
+
"""GET /api/chat?since=<id>: whether chat is on, plus every message
|
|
1411
|
+
newer than <since> (all of the ring when omitted)."""
|
|
1412
|
+
gid, extra = self._ensure_guest_id()
|
|
1413
|
+
try:
|
|
1414
|
+
since = int((qs.get('since') or ['0'])[0])
|
|
1415
|
+
except ValueError:
|
|
1416
|
+
since = 0
|
|
1417
|
+
with _lock:
|
|
1418
|
+
enabled = bool(_party_settings().get('chat'))
|
|
1419
|
+
with _chat_lock:
|
|
1420
|
+
msgs = [m for m in _chat_messages if m['id'] > since]
|
|
1421
|
+
self._json({'enabled': enabled, 'messages': msgs, 'you': 'host' if self._role() == 'admin' else 'guest-' + gid[:4]},
|
|
1422
|
+
headers=extra)
|
|
1423
|
+
|
|
1424
|
+
def _handle_chat_post(self, body):
|
|
1425
|
+
"""POST /api/chat {text, name?}: one message. Refused while chat is
|
|
1426
|
+
off, over-long, empty, or faster than one a second per person."""
|
|
1427
|
+
global _chat_seq
|
|
1428
|
+
with _lock:
|
|
1429
|
+
enabled = bool(_party_settings().get('chat'))
|
|
1430
|
+
if not enabled:
|
|
1431
|
+
return self._json({'error': 'chat is off'}, status=403)
|
|
1432
|
+
gid, extra = self._ensure_guest_id()
|
|
1433
|
+
role = self._role()
|
|
1434
|
+
who = 'admin' if role == 'admin' else gid
|
|
1435
|
+
text = ' '.join(str(body.get('text') or '').split())
|
|
1436
|
+
if not text:
|
|
1437
|
+
return self._json({'error': 'empty message'}, status=400)
|
|
1438
|
+
if len(text) > CHAT_MAX_TEXT:
|
|
1439
|
+
return self._json({'error': f'message too long (max {CHAT_MAX_TEXT} characters)'}, status=400)
|
|
1440
|
+
name = ' '.join(str(body.get('name') or '').split())[:24] or ('host' if role == 'admin' else 'guest-' + gid[:4])
|
|
1441
|
+
now = time.time()
|
|
1442
|
+
with _chat_lock:
|
|
1443
|
+
last = _chat_last_post.get(who, 0)
|
|
1444
|
+
if now - last < CHAT_MIN_INTERVAL:
|
|
1445
|
+
return self._json({'error': 'slow down'}, status=429)
|
|
1446
|
+
_chat_last_post[who] = now
|
|
1447
|
+
_chat_seq += 1
|
|
1448
|
+
msg = {'id': _chat_seq, 'name': name, 'text': text, 'ts': now, 'role': role or 'admin'}
|
|
1449
|
+
_chat_messages.append(msg)
|
|
1450
|
+
del _chat_messages[:-CHAT_KEEP]
|
|
1451
|
+
self._json({'ok': True, 'message': msg}, headers=extra)
|
|
1452
|
+
|
|
1453
|
+
def _handle_chat_clear(self, body):
|
|
1454
|
+
"""Admin: wipe the ring."""
|
|
1455
|
+
with _chat_lock:
|
|
1456
|
+
n = len(_chat_messages)
|
|
1457
|
+
_chat_messages.clear()
|
|
1458
|
+
self._json({'ok': True, 'cleared': n})
|
|
1459
|
+
|
|
1392
1460
|
def _handle_party_played(self, body):
|
|
1393
1461
|
"""Admin: a voted track got played -- clear its tally so the next
|
|
1394
1462
|
vote starts fresh."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weed-cli
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.5
|
|
4
4
|
Summary: Censorship-resistant video PoC — discovery/hosting/download over signed relay events, a real Kademlia DHT, or a TLS-capable NAT-traversal tunnel
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: p2p,video,censorship-resistant,discovery,dht,kademlia,nat-traversal
|
|
@@ -55,6 +55,8 @@ output (not simulated) — see [Status](#status) for what's verified.
|
|
|
55
55
|
- [CLI](#cli)
|
|
56
56
|
- [Interactive shell](#interactive-shell)
|
|
57
57
|
- [Web UI](#web-ui)
|
|
58
|
+
- [Orbit Visualizer](#orbit-visualizer)
|
|
59
|
+
- [Orbit Visualizer plugins](#orbit-visualizer-plugins)
|
|
58
60
|
- [Docker](#docker)
|
|
59
61
|
- [Core mechanisms](#core-mechanisms)
|
|
60
62
|
- [The integrated node](#the-integrated-node)
|
|
@@ -185,9 +187,19 @@ browser opened with that token gets the **party view** and nothing
|
|
|
185
187
|
else — the live Orbit stream, what's playing, a vote on which
|
|
186
188
|
downloaded track plays next (one vote per person per track, toggleable),
|
|
187
189
|
and whatever links you set. The admin's **Party** tab holds the guest
|
|
188
|
-
link and its QR code, the title and links, the live tally with a ▶
|
|
189
|
-
track, and an "auto-play the top vote when a
|
|
190
|
+
link and its QR code, the title and links, the live tally with a ▶ and
|
|
191
|
+
a ♫+ (add to playlist) per track, and an "auto-play the top vote when a
|
|
192
|
+
track ends" switch. The guest page itself is bare: the live picture and
|
|
193
|
+
"now playing" pinned to the top, the vote list scrolling beneath —
|
|
194
|
+
alphabetical, with an iPod-style A–Z strip down the right edge to jump
|
|
195
|
+
by letter, and a "leading:" line for the current front-runners.
|
|
190
196
|
Guests can't host, download, like, subscribe, or read the library.
|
|
197
|
+
Switch on **guest chat** in the Party tab and everyone gets a message
|
|
198
|
+
box: the last few messages are drawn over the picture — into the
|
|
199
|
+
visualizer's canvas, so the stream carries them to VLC, a Roku and the
|
|
200
|
+
guest page's picture alike (the visualizer's 💬 toggles that), and as
|
|
201
|
+
an overlay on the plain player window. Messages live in memory only
|
|
202
|
+
(the last 200), one per second per person, 200 characters each.
|
|
191
203
|
|
|
192
204
|
`--stream-plain-port` adds a plain-HTTP listener that serves *only* the
|
|
193
205
|
stream endpoints (the Orbit MJPEG stream and downloaded files), for
|
|
@@ -201,17 +213,44 @@ Includes real HTTP range support (`/api/stream/<job_id>`) so a
|
|
|
201
213
|
`<video>` tag can seek a completed download instead of downloading it
|
|
202
214
|
blind.
|
|
203
215
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
### Orbit Visualizer
|
|
217
|
+
|
|
218
|
+
The player's 🌀 button opens the **Orbit Visualizer**: a canvas driven by
|
|
219
|
+
the track's audio (Web Audio analyser) and a low-res sample of its
|
|
220
|
+
picture. Eleven built-in modes — Tunnel, Bars, Mirror, Scope, Spiral,
|
|
221
|
+
Pixels, ASCII (six character sets, natural or neon), Plasma, Kaleido,
|
|
222
|
+
Particles, Freefall — plus the ten in `web/orbit_extras.js` (next
|
|
223
|
+
section). Click the lit mode again for the plain video. Speed, React and
|
|
224
|
+
Zoom retune every mode; every setting persists in the browser.
|
|
225
|
+
|
|
226
|
+
- **Fade**: the transition between modes and between tracks — Burn,
|
|
227
|
+
Warp, Glitch, Pixelate, Crossfade, Wipe, the plugin ones, or Random
|
|
228
|
+
(the ⚄ button picks which ones Random may draw from), with a length
|
|
229
|
+
slider. A transition can declare its own length multiple (Win95's
|
|
230
|
+
crash runs at 3×).
|
|
231
|
+
- **Navigation**, Blender-style: scroll zooms, left-drag pans,
|
|
232
|
+
middle-drag rotates, shift+middle pans, ctrl+middle zooms,
|
|
233
|
+
double-click resets. `f` fullscreen, ←/→ cycle modes (fullscreen),
|
|
234
|
+
shift+1-9,0 jump to a mode, ↑/↓ ASCII brightness, `[`/`]` resolution.
|
|
235
|
+
- **Network stream** (📡): the canvas goes out as MJPEG on
|
|
236
|
+
`/api/orbit-view` for VLC (`vlc --demux=mjpeg --network-caching=300
|
|
237
|
+
<url>`; 🔗 copies the URL, token included), a Roku IP-camera viewer,
|
|
238
|
+
or the guest party page. Resolution and JPEG quality are selectable;
|
|
239
|
+
the ⏱ slider delays the local audio by up to 10 s to line up with a
|
|
240
|
+
laggy viewer. The stream keeps running while the visualizer is closed,
|
|
241
|
+
minimised to PIP, or in a background tab. Picture only — audio stays
|
|
242
|
+
wherever the browser is playing it.
|
|
243
|
+
- **MIDI** (🎹): an AKAI MPK mini or any Web MIDI controller
|
|
244
|
+
(Chrome/Edge/Firefox). Pads pick modes and fire actions, knobs turn
|
|
245
|
+
the sliders or sweep through modes/transitions/character sets;
|
|
246
|
+
endless encoders are detected on their own. Every row, plugin modes
|
|
247
|
+
and transitions included, has a Learn button; **export** saves the
|
|
248
|
+
keymap as a `.json` file and **import** loads one.
|
|
249
|
+
- **Video swap** (⇄ on the player): another download's picture stands
|
|
250
|
+
in for the current track's — for an mp3 or a static-image video, so
|
|
251
|
+
the visuals have real footage. Audio stays with the track; the
|
|
252
|
+
borrowed video loops on its own, follows play/pause, and the pairing
|
|
253
|
+
is remembered per track.
|
|
215
254
|
|
|
216
255
|
### Orbit Visualizer plugins
|
|
217
256
|
|
|
@@ -444,8 +483,13 @@ just designed:
|
|
|
444
483
|
- Transitive trust through the subscribe graph
|
|
445
484
|
- NAT traversal via relay-mediated tunneling, with TLS and heartbeat
|
|
446
485
|
- Kademlia DHT discovery, survives the announcing node going offline
|
|
447
|
-
- Local web UI with live progress, QR onboarding,
|
|
448
|
-
-
|
|
486
|
+
- Local web UI with live progress, QR onboarding, HTTP range streaming,
|
|
487
|
+
optional two-tier token auth, and the guest party page with voting
|
|
488
|
+
- Orbit Visualizer: 21 modes and 16 transitions behind a plugin API,
|
|
489
|
+
MJPEG network stream (VLC/Roku), Web MIDI control with keymap files,
|
|
490
|
+
video swap — 44 Playwright tests against real Chromium (`tests/e2e/`)
|
|
491
|
+
- Containerized node (`Dockerfile.node`, `docker-compose.node.yml`),
|
|
492
|
+
with `web/` bind-mounted so frontend edits need no rebuild
|
|
449
493
|
|
|
450
494
|
## Known limitations
|
|
451
495
|
|
|
@@ -484,3 +528,7 @@ mechanisms hold up:
|
|
|
484
528
|
and votes are anonymous per browser cookie — enough to put a
|
|
485
529
|
LAN-bound UI behind something and hand guests a safe subset, not a
|
|
486
530
|
reason to face it at the internet.
|
|
531
|
+
- The Orbit stream is picture only, as MJPEG: fine for VLC, an
|
|
532
|
+
IP-camera app or the party page, but no audio travels with it and
|
|
533
|
+
there's no HLS/MP4 endpoint, so a stock Roku or smart-TV player
|
|
534
|
+
can't take it directly.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|