Compare commits

...

2014 commits
v2.3.1 ... 3.0

Author SHA1 Message Date
Speng Reb
20daaa780f Remove list of shows in shows tab - we can use the calendar from now on 2026-06-02 00:05:06 +02:00
Speng Reb
e917ea25eb Gcalendar can sync as background task 2026-06-01 23:51:27 +02:00
Speng Reb
efd5fe0465 Gcalendar now can batch jobs and queue create and delete. Keeps a local record of gcalendar records. 2026-06-01 23:43:30 +02:00
Speng Reb
6b40b9c500 After running a stress test a couple times i found some nasty edge cases and tried to patch them up as best i could 2026-06-01 21:36:54 +02:00
Speng Reb
03711e4726 Update bot API docs for new shows API 2026-06-01 14:42:08 +02:00
Speng Reb
dd1bf9d55b Add many UX improvements to channel schedule 2026-06-01 01:46:00 +02:00
Spengreb
4ec1e83337
Merge pull request #16 from Spengreb/google-calendar-sync
Google calendar sync
2026-06-01 00:24:24 +02:00
Spengreb
c102b90ef6
Merge pull request #15 from Spengreb/schedule
Calendar for showing scheduled events
2026-06-01 00:23:53 +02:00
Speng Reb
71b0a092ca Google calendar sync v1 2026-05-31 23:56:37 +02:00
Speng Reb
60c6a50d9e Add calendar for displaying scheduled shows per channel 2026-05-31 22:57:20 +02:00
Speng Reb
c977cbd754 Basic channel schedule 2026-05-31 22:24:43 +02:00
Speng Reb
7f8afe23cd Playlist items added from shows are always temp 2026-05-31 21:53:50 +02:00
Speng Reb
49623df29d Fix CSRF issues from previous commits 2026-05-31 15:06:06 +02:00
Spengreb
2788dae3c8
Merge pull request #14 from Spengreb/broadspectrum-code-analysis
Update to node 20 and jQuery 3
2026-05-21 21:18:27 +02:00
Speng Reb
36da4bdff1 Harden API and session security: enforce CSRF on cookie-auth /api/v1 writes, exempt bot bearer tokens, and set SameSite=Lax + conditional Secure on auth/CSRF/ip-session cookies 2026-05-21 16:25:34 +02:00
Speng Reb
6eeee342d7 Protect /api/v1 mutations with CSRF for cookie auth while exempting cbt_ bearer bot tokens and wiring UI X-CSRF-Token headers 2026-05-21 16:23:30 +02:00
Speng Reb
12696452aa Fix shows/bot API auth gaps, handle missing channels as 404, make recurrence DST-safe, and clear lint regressions 2026-05-21 16:13:56 +02:00
Speng Reb
e3dd961430 Add example python bot for making shows 2026-05-21 16:06:00 +02:00
Spengreb
03922e8484
Merge pull request #13 from Spengreb/emote-substring-search-ux
Emote substring search ux
2026-05-21 15:06:22 +02:00
Speng Reb
73c90d8802 Merge branch 'emote-substring-search' into emote-substring-search-ux 2026-05-21 15:04:26 +02:00
Speng Reb
341b91aad1 Improve UX for emote auto complete 2026-05-21 15:03:56 +02:00
Spengreb
b4e93cc63c
Merge pull request #12 from Spengreb/bugfix/show-force-start-issue
Fix bug where force start did not force starting the show
2026-05-21 14:15:21 +02:00
Speng Reb
c49ff4bac1 Fix bug where force start did not force starting the show 2026-05-21 14:14:47 +02:00
Spengreb
5209c1c10a
Merge pull request #9 from Spengreb/schedules-shows
Scheduled shows
2026-05-21 13:47:59 +02:00
Spengreb
8e9bd64e5b
Merge pull request #11 from Spengreb/emote-selector-better-modal
Emote selector can be resized
2026-05-21 13:47:29 +02:00
Speng Reb
ae037c7795 Emote selector can be resized 2026-05-21 13:46:51 +02:00
Spengreb
1050a15ef6
Merge pull request #10 from Spengreb/bugfix/tvmode
Fix bug where if Big Picture mode was set as default layout switching…
2026-05-21 13:01:15 +02:00
Speng Reb
25d4be7aae Fix bug where if Big Picture mode was set as default layout switching layouts would look weird 2026-05-21 13:00:36 +02:00
Speng Reb
c4ee655d15 Shows playlist editor now shows media title instead of ID 2026-05-20 21:10:49 +02:00
Speng Reb
56ab732f6b Better handling of TZ and Bot API added 2026-05-20 21:00:48 +02:00
Speng Reb
17f38874d1 Add a scheduled show concept to the project without bot API for now 2026-05-20 20:52:26 +02:00
Spengreb
4d61a68e8b
Merge pull request #8 from Spengreb/channel-api-bot-life
Channel API for Bots
2026-05-20 17:09:51 +02:00
Speng Reb
2bdd975c3c Add python example bot using lib 2026-05-05 01:34:00 +02:00
Speng Reb
0c15e06975 merge 3.0 2026-05-04 16:40:32 +02:00
Spengreb
8fb51e6dc3
Merge pull request #7 from Spengreb/ez-local-dev
Add docker compose setup
2026-05-04 16:37:24 +02:00
Speng Reb
aa5d4a1850 Add docker compose setup 2026-05-04 16:26:32 +02:00
Speng Reb
dc70e1236b Initial bot API v1 2026-05-04 16:07:59 +02:00
BigLargeExtraDelicious
914605f393 Improve emote autocomplete to match substrings 2026-05-03 20:10:00 +01:00
Spengreb
2a62e6df90
Merge pull request #5 from Spengreb/emote-suggest
Emote suggest
2026-04-21 17:52:48 +02:00
Speng Reb
6281b0ead3 Emote compact and infinite scroll emote list 2026-04-21 17:51:03 +02:00
Speng Reb
e7111689f1 Compact emote suggestions 2026-04-21 17:37:45 +02:00
Spengreb
9d313e1375
Merge pull request #4 from Spengreb/remove-bad-protoswitching
Remove bad protoswitching from previous commit
2026-04-21 00:27:51 +02:00
Speng Reb
6efb8902fa Remove bad protoswitching from previous commit 2026-04-21 00:27:20 +02:00
Spengreb
80cd107aa0
Merge pull request #3 from Spengreb/tv-layout
Add TV layout mode
2026-04-21 00:11:45 +02:00
Speng Reb
f3cfe74cfa Add TV layout mode 2026-04-21 00:08:25 +02:00
Spengreb
917b227ff5
Merge branch 'calzoneman:3.0' into 3.0 2026-03-18 23:17:04 +01:00
Xaekai
589f999a9c Fix bitchute queuing 2025-11-06 18:02:23 -08:00
Xaekai
eac1547aea Resolve #1011 2025-11-06 15:58:48 -08:00
dependabot[bot]
c1e050c26e
Bump semver from 5.7.1 to 5.7.2 (#972)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 01:11:39 -07:00
dependabot[bot]
1c3025ceee
Bump word-wrap from 1.2.3 to 1.2.4 (#974)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 01:11:27 -07:00
dependabot[bot]
e13d5b69c8
Bump postcss from 8.4.21 to 8.4.31 (#976)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 01:11:13 -07:00
Spengreb
7de1894a0c
Merge pull request #1 from Spengreb/whep-streems
Whep streems
2025-07-22 18:36:11 +02:00
Speng Reb
2aabaf6b6d Update whepplayer to be a generic player instead of messing with playerJS 2025-07-22 18:33:14 +02:00
Speng Reb
f12115159e Accepts whep from streem.vereto.net 2025-07-22 01:02:34 +02:00
anonanonanon88
bb5173fd12 updated package-lock.json for node 22 2025-02-22 22:25:10 -08:00
anonanonanon88
6416b4a2b6 Uptated nan version in package-lock.json for node 22 compatibility 2025-02-22 22:25:10 -08:00
Zankaria
9738c3f8c8 Add support for unix socket connections to the mysql database 2024-09-19 19:25:10 -07:00
Honore Doktorr
adc0ea27a9 Add player integration code removed from the dailymotion js sdk
Restores https://github.com/dailymotion/dailymotion-sdk-js commit 75b4102
2024-05-26 17:30:11 -07:00
Calvin Montgomery
4c437efb5d Fix #981 2024-04-18 20:08:59 -07:00
dependabot[bot]
227244e2d0 Bump socket.io-parser from 4.2.1 to 4.2.3
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.1 to 4.2.3.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.1...4.2.3)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-28 21:12:55 -07:00
Calvin Montgomery
6f47ed42db Bump mediaquery 2023-05-28 21:12:36 -07:00
Kethsar
98bfb6736e Remove string template around maxlength property for chat input 2023-03-25 14:31:25 -07:00
Kethsar
2c541448a2 Set the cap for max-chat-message-length to 1000 2023-03-25 14:31:25 -07:00
Kethsar
21d7f16413 Fix missed expansion of the option 2023-03-25 14:31:25 -07:00
Kethsar
87198bd4e7 Expand chat message length option to be consistent with other options 2023-03-25 14:31:25 -07:00
Kethsar
986207b46b Add max chat message length config option 2023-03-25 14:31:25 -07:00
Kethsar
ed410fdebe Update mediaquery dependency hash 2023-03-25 14:29:56 -07:00
Calvin Montgomery
1a9d920884 Detect old browser JS engines 2023-01-28 19:41:39 -08:00
Calvin Montgomery
c78ef333da Fix a couple issues discussed on IRC 2023-01-11 17:57:02 -08:00
Calvin Montgomery
fad1da7ab4 deps: fix high sev warnings 2023-01-10 20:56:38 -08:00
Calvin Montgomery
d37e69e1a6 Update package-lock for nan so that node v19 builds successfully 2022-10-30 18:10:19 -07:00
Calvin Montgomery
1e2dcee4fa Update NEWS 2022-09-23 21:39:38 -07:00
Calvin Montgomery
6ec2f3d491 Fix todo 2022-09-23 21:39:38 -07:00
Calvin Montgomery
306e3adde8 Work around flaky test 2022-09-23 21:39:38 -07:00
Calvin Montgomery
99740a3673 Add cache, test 2022-09-23 21:39:38 -07:00
Calvin Montgomery
913348d46e Continue working on banned channels 2022-09-23 21:39:38 -07:00
Calvin Montgomery
ae5dbf5f48 Continue working on banned channels 2022-09-23 21:39:38 -07:00
Calvin Montgomery
8338fe2f25 Work on banned channels feature 2022-09-23 21:39:38 -07:00
Xaekai
7921f41174 Fix inadvertent code reversions 2022-09-18 20:04:42 -07:00
Calvin Montgomery
50e2692896 Fix update mediaquery git hash 2022-09-18 19:10:36 -07:00
Calvin Montgomery
9e0f7b8efa Tweaks 2022-09-18 19:10:36 -07:00
Xaekai
fd9586e0da Update custom manifest documentation regarding audioTracks 2022-09-18 19:10:36 -07:00
Xaekai
f185e6c3ea Add audioTracks support for custom manifests 2022-09-18 19:10:36 -07:00
Xaekai
2cf26cdc4c Add disposal to audio switcher 2022-09-18 19:10:36 -07:00
Xaekai
008c24f892 Add compiled JSO libraries 2022-09-18 19:10:36 -07:00
Xaekai
a398e3a6fa Track last chatMsg time, and ignore reconnect spam 2022-09-18 19:10:36 -07:00
Xaekai
aa04f0d034 Add vjs plugin for audio track switching 2022-09-18 19:10:36 -07:00
Xaekai
e7f0aa98be Move add to be first playlist control 2022-09-18 19:10:36 -07:00
Xaekai
0f9d778a27 Eliminate jQuery in index template microscript 2022-09-18 19:10:36 -07:00
Xaekai
119b6a62b8 Focus searchbox when emotelist modal is shown 2022-09-18 19:10:36 -07:00
Xaekai
9d00d9666d Fix Nicovideo methods 2022-09-18 19:10:36 -07:00
Xaekai
f6ba5b71e8 Update vjs components
Upgrade Video.js core to v7.18.0 from v5.10.7
Upgrade Dash.js to v4.2.8 from v2.6.3
Upgrade videojs-contrib-dash to v5.1.1 from v2.9.1
Modify videojs-resolution-switcher
2022-09-18 19:10:36 -07:00
Xaekai
9b05e2eb8c Move Video.js components to a subfolder 2022-09-18 19:10:36 -07:00
Xaekai
911558760f Remove all references to wmode
Usage of wmode was specific to Flash, which is long dead.
2022-09-18 19:10:36 -07:00
Xaekai
45217ccad8 Add Niconico support 2022-09-18 19:10:36 -07:00
Xaekai
aeb5de85b6 Update HLS support 2022-09-18 19:10:36 -07:00
Xaekai
53911ab9f0 Reorganize PlayerJSPlayer dependents 2022-09-18 19:10:36 -07:00
Xaekai
a2c4ea5036 Add Odysee support 2022-09-18 19:10:36 -07:00
Xaekai
517058bef3 Set videojs poster on player ready
Resolves Github issue #870
2022-09-18 19:10:36 -07:00
Xaekai
1790d5b569 Add BandCamp support 2022-09-18 19:10:36 -07:00
Xaekai
97b8d1b4b7 Enable caching BitChute metadata 2022-09-18 19:10:36 -07:00
Xaekai
25ddc336e0 Use child iframe for BitChute
By using an iframe we can take advantage of the referrer meta tag,
while still being able to scaffold everything relatively easily because it's same-origin
2022-09-18 19:10:36 -07:00
Xaekai
498272b128 Flash is long dead 2022-09-18 19:10:36 -07:00
Xaekai
26f6611ca8 Options to autoembed PeerTube 2022-09-18 19:10:36 -07:00
Xaekai
6b831bc367 Touch up data.js
Reorder useropts to match client
Remove long unused variable
2022-09-18 19:10:36 -07:00
Xaekai
ffd01fe30b Fix issue with queue progress
If the user queues a PeerTube link with a long uuid the progress bar would never go away. Now it will just check against the hostname.
2022-09-18 19:10:36 -07:00
Xaekai
8774dc89e7 Fixup Livestream.com 2022-09-18 19:10:36 -07:00
Xaekai
16f183c117 Add BitChute support 2022-09-18 19:10:36 -07:00
Xaekai
ba80c1591d Fixup various lint
Touched up callbacks and paginator
2022-09-18 19:10:36 -07:00
Xaekai
4fada9a8d2 Eliminate jQuery from inline js/css charlimit notice 2022-09-18 19:10:36 -07:00
Xaekai
7441892235 Eliminate jQuery event shorthands 2022-09-18 19:10:36 -07:00
Xaekai
f929758bfd Improve the ESLint situation 2022-09-18 19:10:36 -07:00
Xaekai
500f295506 Allow for the omission of particular frames in SOCKET_DEBUG
In particular, mediaUpdate spam.
2022-09-18 19:10:36 -07:00
Xaekai
de1f37735b EmoteList live reconfig support 2022-09-18 19:10:36 -07:00
Xaekai
9f9bbfa022 Update jQuery and jQuery UI 2022-09-18 19:10:36 -07:00
Xaekai
d516c5ebfc Add PeerTube support 2022-09-18 19:10:36 -07:00
Xaekai
3668c1b3da Refactor parseMediaLink 2022-09-18 19:10:36 -07:00
Xaekai
0e3307b9f4 Remove references to defunct services
Imgur discontinued support for albums
SmashCast/Hitbox disappeared
Ustream was sunset by IBM
Mixer is dead
Picasa is long dead
Vidme is long dead
IE11 is dead
2022-09-18 19:10:36 -07:00
Xaekai
3ea16944d2 Ignore patch files 2022-09-18 19:10:36 -07:00
Calvin Montgomery
dcfcee9a23 Accept #946 2022-05-17 21:13:50 -07:00
Calvin Montgomery
fd451fe9d2 Require at least one vote to skip 2022-05-09 20:25:34 -07:00
Calvin Montgomery
cc283c0be9 Switch mediaquery back to githash instead of npm 2022-04-23 19:41:00 -07:00
Calvin Montgomery
c9da64107f Upgrade a couple deps to shut up npm audit 2022-04-23 19:36:51 -07:00
dependabot[bot]
5b92ea0660 Bump node-fetch from 2.6.1 to 2.6.7
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-03 09:42:45 -07:00
dependabot[bot]
facc72b22d Bump nodemailer from 6.5.0 to 6.6.1
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 6.5.0 to 6.6.1.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.5.0...v6.6.1)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-03 09:42:22 -07:00
dependabot[bot]
578c0f0ddc Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-03 09:38:45 -07:00
Xaekai
e099781686 Revert 3dfa587
The issue was caused by Babel weirdness.
2022-03-06 19:41:30 -08:00
Calvin Montgomery
3dfa587739
Update google-drive-subtitles.md 2022-02-05 18:59:29 -08:00
Calvin Montgomery
0d9f4a5f03 Fix cookies on ACP for SIO4 upgrade 2021-11-06 19:53:16 -07:00
Techanon
ab8faf7c99 Fix chat width resizing when window is very thin
When the window resized to a small width, the chat header buttons would wrap to the next line, but would inline with the chat box itself making it resize to unreadable widths.
Changing the header to flex with some minor adjustments prevents the inline wrapping thus the chatbox retains it's intended width.
2021-11-05 16:14:15 -07:00
Calvin Montgomery
7c3f3070f9 Fix bug introduced by fixing #918 2021-10-17 16:37:57 -07:00
Calvin Montgomery
1bab65bb13 Bump some devdeps to shut up npm audit 2021-10-13 20:19:28 -07:00
dependabot[bot]
01063c2623 Bump nth-check from 2.0.0 to 2.0.1
Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-13 20:16:30 -07:00
Calvin Montgomery
bd63013524 Fix #925 2021-10-13 20:14:44 -07:00
Calvin Montgomery
af62fbaef4 Fix #924 2021-10-13 20:12:31 -07:00
Calvin Montgomery
f41e0bda82 Fix new messages indicator being hidden behind chat messages on chromium 2021-10-13 19:58:19 -07:00
dependabot[bot]
0d8dcc41b2 Bump tar from 6.1.5 to 6.1.11
Bumps [tar](https://github.com/npm/node-tar) from 6.1.5 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.5...v6.1.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-05 09:57:03 -07:00
Calvin Montgomery
d179cd896f Allow revoting without refreshing 2021-08-19 21:03:15 -07:00
Calvin Montgomery
1f10f0f09c Fix eslint error 2021-08-19 20:55:40 -07:00
Calvin Montgomery
edb5f94b7c Add a POST flow to password recovery (#871) 2021-08-19 20:55:02 -07:00
Calvin Montgomery
d563a85092 Use embed src as url in playlist for custom embed 2021-08-19 20:46:38 -07:00
Calvin Montgomery
394f03ee1c Remove some legacy cruft 2021-08-19 20:44:57 -07:00
Calvin Montgomery
7214b7c474 Upgrade to socket.io v4 2021-08-19 20:36:04 -07:00
Calvin Montgomery
1b7e7c74f5 Remove legacy counters 2021-08-19 20:36:04 -07:00
Calvin Montgomery
11a0cd79bb Fix test 2021-08-12 19:48:06 -07:00
Calvin Montgomery
5f799fe1a1 Disable soundcloud lookup due to #916 2021-08-12 19:46:47 -07:00
Calvin Montgomery
c717a55c2d Implement #884 2021-08-11 21:16:19 -07:00
Zero
9a008d4623 Add support for raw AV1/Opus 2021-08-10 21:14:03 -07:00
dependabot[bot]
47d268335e Bump path-parse from 1.0.6 to 1.0.7
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-10 21:05:05 -07:00
dependabot[bot]
f136a02240 Bump tar from 6.1.0 to 6.1.5
Bumps [tar](https://github.com/npm/node-tar) from 6.1.0 to 6.1.5.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.0...v6.1.5)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-10 21:03:42 -07:00
Calvin Montgomery
a33d1e12d2 Fix #918 2021-08-10 21:03:13 -07:00
Calvin Montgomery
337e8cd1d3 Add some big ol nags about no support for gdrive 2021-08-08 09:49:20 -07:00
Calvin Montgomery
adfe26aad1 Bump version 2021-08-02 19:24:40 -07:00
Calvin Montgomery
f84892dc6a Refactor polls 2021-08-02 19:23:53 -07:00
Calvin Montgomery
c290f9fcca deps: bump cheerio to rc10 to resolve dependabot alert 2021-07-25 20:49:37 -07:00
Calvin Montgomery
d85c4ec84b Remove old player that isn't used anymore 2021-07-25 20:46:32 -07:00
Calvin Montgomery
bce5d0d878 player/youtube: remove setQuality logic due to #726 2021-07-25 20:43:15 -07:00
Calvin Montgomery
a3c17ea8ea Fix #913 2021-07-22 21:55:23 -07:00
dependabot[bot]
982c6fbfab Bump ws from 7.4.4 to 7.4.6
Bumps [ws](https://github.com/websockets/ws) from 7.4.4 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.4...7.4.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 21:20:19 -07:00
dependabot[bot]
709963fd81 Bump browserslist from 4.16.3 to 4.16.6
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.3 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.3...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 21:19:53 -07:00
dependabot[bot]
1f4f9a9c3e Bump postcss from 8.2.8 to 8.2.15
Bumps [postcss](https://github.com/postcss/postcss) from 8.2.8 to 8.2.15.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.2.8...8.2.15)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 21:19:46 -07:00
dependabot[bot]
b621a1b327 Bump redis from 3.0.2 to 3.1.1
Bumps [redis](https://github.com/NodeRedis/node-redis) from 3.0.2 to 3.1.1.
- [Release notes](https://github.com/NodeRedis/node-redis/releases)
- [Changelog](https://github.com/NodeRedis/node-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NodeRedis/node-redis/compare/v3.0.2...v3.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 21:19:24 -07:00
Calvin Montgomery
d28be04416 Fix package-lock version 2021-04-04 14:27:43 -07:00
Calvin Montgomery
db08272416 deps: upgrade some devdeps 2021-04-04 14:27:04 -07:00
Calvin Montgomery
8562b2173e Add placeholder text for #877 2021-03-22 22:58:38 -07:00
Calvin Montgomery
da53decdd5 Fix #885 2021-03-22 22:53:03 -07:00
Calvin Montgomery
05107ce13f Remove ignore button from self (#904) 2021-03-22 22:49:11 -07:00
Calvin Montgomery
56b4ec8f3a bump version 2021-03-21 21:50:05 -07:00
Calvin Montgomery
a1c9ae3626 deps: check in package-lock.json 2021-03-21 21:50:05 -07:00
Calvin Montgomery
08c0cfcd58 deps: upgrade prom-client 2021-03-21 21:50:05 -07:00
Calvin Montgomery
5f3d0859fd deps: bump knex and mysql 2021-03-21 21:50:05 -07:00
Calvin Montgomery
988029e6c7 deps: bump uuid 2021-03-21 21:50:05 -07:00
Calvin Montgomery
0b57f528bf deps: bump sanitize-html 2021-03-21 21:50:05 -07:00
Calvin Montgomery
99559d8fda deps: remove graceful-fs
graceful-fs was added at a time when channel state was stored in
flatfiles that could become corrupted if enough concurrent saves
occurred to hit the ulimit for maxfds (EMFILE).  Saving channels this
way is no longer supported, so it shouldn't be an issue anymore.
2021-03-21 21:50:05 -07:00
Calvin Montgomery
811a7c4d48 deps: bump cheerio 2021-03-21 21:50:05 -07:00
Calvin Montgomery
182e6f0816 customembed: drop <object> and <embed> 2021-03-21 21:50:05 -07:00
Calvin Montgomery
9e5a63d880 dep upgrades part 1 2021-03-21 21:50:05 -07:00
Calvin Montgomery
bb165606d6 Add explicit integ test for old password hash verification 2021-03-21 21:50:05 -07:00
Calvin Montgomery
7b56f3f0e7 Bump copyright year in LICENSE 2021-03-21 21:50:05 -07:00
kr4ssi
e391a80d65 Allow alt-attribute on <img>-tags
https://www.w3.org/html/wg/wiki/IssueAltAttribute
2021-01-28 19:32:33 -09:00
Calvin Montgomery
a75917d4e4 fix: attempt to avoid socket leak in node >= 13.x
The default timeout was removed from the HTTP module in node 13.x:
https://github.com/nodejs/node/pull/27558.  I believe this is the most
likely cause of fd leaks when running under current node versions.
2021-01-16 14:19:22 -08:00
Calvin Montgomery
00e9acbe4d Revert "Remove channel reference counter"
This reverts commit d678fa56d1.  The
reference counter, flawed as it is, was masking far more issues than I
realized.  It would require a more significant rearchitecture of the
code to remove it.  Probably better to keep it and try to improve it for
now.
2021-01-09 13:03:38 -08:00
aleves64
3262f7822f Delete package-lock.json 2021-01-03 14:48:10 -09:00
aleves64
a8d9781821 Small changes 2021-01-03 14:48:10 -09:00
aleves64
18fd611c91 Links to wiki now 2021-01-03 14:48:10 -09:00
aleves64
7c3d2f74ed Made get-info save if yt video is age-restricted and made playlist refuse to add age-restricted videos 2021-01-03 14:48:10 -09:00
Calvin Montgomery
9e3c23c58a Refuse to start on invalid config 2020-12-02 18:09:49 -08:00
Calvin Montgomery
d678fa56d1 Remove channel reference counter
This was an old attempt at gracefully unloading channels that still had
pending callbacks.  Its implementation was always flawed, and the number
of places where it was used is small enough to replace with
straightforward checks for whether the channel has been unloaded after
an asynchronous operation.  Hopefully fixes the stuck 0 user channels
issue.
2020-11-11 22:05:05 -08:00
Calvin Montgomery
66fadab492 Handle some common error conditions in the ffprobe preflight path 2020-11-11 22:05:05 -08:00
animeavi
750509eaf1 Support enabling custom media subtitle by default
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track#Attributes
2020-11-09 18:37:45 -09:00
Calvin Montgomery
8fc951350e Bump gcc version in travis.yml 2020-10-23 13:46:26 -07:00
Calvin Montgomery
3f9a0aaf05 Fix npm jank 2020-10-23 11:51:59 -07:00
Calvin Montgomery
801e54afa2 Tweak urlencoded body size limit 2020-09-22 20:23:46 -07:00
deerfarce
6f8bde91e1 adjust sendUserMeta rank comparison
includes users with the same rank as the minimum rank for the action when sending the meta frame
2020-09-22 20:12:09 -07:00
Calvin Montgomery
df82d2d4f1 Add registration captcha support 2020-09-22 20:11:34 -07:00
Calvin Montgomery
f08cce5aed Update some devdeps, resolves some npm audit warnings 2020-08-21 20:47:01 -07:00
Calvin Montgomery
962998c543 deps: bump bcrypt to 5.0.0 2020-08-21 20:37:16 -07:00
Calvin Montgomery
80d3d14c85 Add integ test for verifyLogin 2020-08-21 20:31:54 -07:00
Calvin Montgomery
f081bc782a RIP Mixer 2020-07-26 10:24:36 -07:00
Lewis Crichton
99af92ed2c Make option on by default 2020-07-18 18:43:47 -07:00
Lewis Crichton
c148c991cd Relocate and rename option, make option restore after page refresh. 2020-07-18 18:43:47 -07:00
Lewis Crichton
d4f75146c7 option to disable IP on userlist hover 2020-07-18 18:43:47 -07:00
Calvin Montgomery
4598a6a58c Wrap chat input in fake form to appease chrome's heuristics (#807) 2020-07-03 11:55:17 -07:00
Calvin Montgomery
248c200a74 Implement twitch changes for #874 2020-06-22 19:39:00 -07:00
Calvin Montgomery
b70194c8f2 Add destroy cb for dailymotion (#873) 2020-06-19 18:31:25 -07:00
Calvin Montgomery
ffa10648e4 Update travis.yml: drop 10, 13, add 14 2020-06-18 21:25:54 -07:00
Calvin Montgomery
4f5cd7d741 Fix UI bug (#872) 2020-06-18 21:23:05 -07:00
Calvin Montgomery
a85b379f17
Fix old 6irc ref 2020-06-09 23:54:38 -07:00
Calvin Montgomery
9e5fcf4904 Fix #866 2020-05-17 10:42:21 -07:00
Calvin Montgomery
529a3561ca Set user-agent in ffprobe pre-flight check (#869) 2020-05-17 10:42:21 -07:00
Xaekai
88365612da
Replace userlist visibility check logic (#859)
* Replace visibility check logic

JQuery queries using getComputedStyle, which makes it impossible to change userlist behavior using CSS. This replaces the check with a direct style="" value check so the JS does not trip up if any CSS customizations to the list visibility were made.

Co-authored-by: Algoinde <algoinde@gmail.com>
2020-04-17 14:53:39 -07:00
Xaekai
f2adbe18da
Explicitly use UTF8 encoding for media metadata table (#863) 2020-04-11 14:23:36 -07:00
Xaekai
a53f65a1d5 Fix channel password prompt
Add zin param to allow explicit z-index in JS generated dialog boxes.
Give needpass dialog explicit z-index
2020-03-29 10:31:27 -07:00
Calvin Montgomery
47bb3e47a2 Add metric for yt cached result age 2020-03-20 19:54:34 -07:00
Calvin Montgomery
83fd8f11b2 Fix updated_at in media_metadata_cache 2020-03-20 19:44:11 -07:00
Calvin Montgomery
5a386d0f81 Remove fallback to YT from library search 2020-03-20 19:38:48 -07:00
kr4ssi@tuta.io
d2358924a4 Add tabcompletion for PMs 2020-03-16 20:08:04 -07:00
Calvin Montgomery
106065184f Remove old flatfile chandump storage 2020-02-15 16:17:49 -08:00
Calvin Montgomery
e3a9915b45 Clean up a few things that no longer work/are no longer used 2020-02-09 16:50:37 -08:00
Calvin Montgomery
b80a532f9e Add YouTube cache table 2020-02-09 15:49:38 -08:00
Calvin Montgomery
46311bd661 Add missed file 2020-01-26 20:20:37 -08:00
Calvin Montgomery
58e4e09840 Replace twitch clip player (#842) 2020-01-26 20:17:55 -08:00
Calvin Montgomery
c809b1994a Fix redirect logic for ffprobe pre-flight check 2020-01-11 11:24:34 -08:00
Olie440
842d0bb4be update button labels (#839)
Update video search button labels to use a consistent verb.
2019-12-05 20:32:23 -08:00
Calvin Montgomery
40b5a0fe83 Upgrade knex 2019-12-01 16:29:52 -08:00
Calvin Montgomery
4d3c90f5ee Sunset node v8, add node v13 to travis 2019-12-01 16:04:55 -08:00
Calvin Montgomery
9aa73bee7c Remove --loose babel parameter that throws errors now 2019-12-01 16:04:16 -08:00
Calvin Montgomery
b0b22a7579 Fix migrator (#831) 2019-10-27 13:09:22 -07:00
Calvin Montgomery
06b3916a6c Fix #822 2019-10-26 20:43:44 -07:00
Calvin Montgomery
c4a1d4b18c Add dummy ID to migrator to fix #831 2019-10-26 20:35:42 -07:00
Calvin Montgomery
08f9feef74 Match word boundaries for nick highlight (#819) 2019-08-01 20:02:37 -07:00
Calvin Montgomery
1ec3eab0dc Preserve current playing item when shuffling (#812) 2019-08-01 19:57:32 -07:00
Calvin Montgomery
27e8885285 Add kick logline (#821) 2019-08-01 19:49:22 -07:00
Calvin Montgomery
77b7af7fd9 deps: bump cytubefilters 2019-06-27 20:51:58 -07:00
Calvin Montgomery
5c51d73c4e Update nodemailer 2019-06-16 13:41:51 -07:00
mrx1983
959ef89c27 add autoplay attribute to custom embed iframe tag, so autoplay works as expected
calzone said i should make a pull request. so here it is.
it would be great if 'allow="autoplay"' attribute is added to the generated iframe for custom embeds.
so autoplay works as expected.
2019-06-09 10:33:25 -07:00
Calvin Montgomery
5a2494adcf Prevent uncaught exception if spawn() throws synchronously (e.g. ENOMEM) 2019-05-28 21:32:03 -07:00
Calvin Montgomery
6b2dfa483c Fix #813 2019-05-25 16:07:44 -07:00
Calvin Montgomery
df934f401c Add clarity to custom media doc about how the user must host the JSON file 2019-05-05 14:17:59 -07:00
Calvin Montgomery
60c348a905 Clamp timeouts to 1 day 2019-04-28 22:30:08 -07:00
Calvin Montgomery
2a1f1df17b Bump some crufty dependencies 2019-04-28 11:18:04 -07:00
Calvin Montgomery
97266b6dfc Better fix for jank dailymotion race conditions 2019-04-11 20:43:24 -07:00
Calvin Montgomery
a3a2daff4c Remove file extension check (#801) 2019-04-07 16:32:58 -07:00
Calvin Montgomery
5493a81611 Revert "Fix #799 and remove old unused quality selection"
This reverts commit 8c136c563a.
2019-03-30 12:52:17 -07:00
Calvin Montgomery
13c468c768 Fix test 2019-03-27 21:37:57 -07:00
Calvin Montgomery
12924b9b5a Implement #786 2019-03-27 21:33:16 -07:00
Calvin Montgomery
c5b122bcf8 Fix #790 2019-03-27 21:28:46 -07:00
Calvin Montgomery
96bf3df928 Fix #793 2019-03-27 21:26:06 -07:00
Calvin Montgomery
8c136c563a Fix #799 and remove old unused quality selection 2019-03-27 21:19:30 -07:00
Calvin Montgomery
b25560c4a9 Add error message for #798 2019-03-27 21:05:45 -07:00
Calvin Montgomery
cb95aaa4e8 Error on node < 8 2019-03-27 20:33:54 -07:00
Calvin Montgomery
367df3d70b Implement #797 2019-02-23 21:23:21 -08:00
Calvin Montgomery
c6f9b1611e Add some sanity checks for common first-startup issues 2019-02-10 10:22:16 -08:00
Calvin Montgomery
66d81ffb22 Update YouTube instructions (#792) 2019-02-02 15:59:02 -08:00
Calvin Montgomery
a81e4d1d16 Fix copyright year in LICENSE 2019-02-02 15:56:43 -08:00
Calvin Montgomery
dfb7177a6d Add workaround for Dailymotion issue 2019-02-02 15:56:20 -08:00
Calvin Montgomery
5c76eaf68a
Remove typo in custom-media.md 2018-12-29 14:42:06 -08:00
Calvin Montgomery
8d0c1a03d1 Add inactive column to base schema 2018-12-18 19:10:53 -08:00
Calvin Montgomery
c85be71f23 delet undefined 2018-12-07 21:11:40 -08:00
Calvin Montgomery
9c44488d8e Add sanity check to prevent null duration from corrupting playlist meta 2018-12-07 20:47:46 -08:00
Calvin Montgomery
693c0e8673 Update NEWS for account deletion 2018-12-07 20:36:53 -08:00
Calvin Montgomery
b68ed4d77a Set purge interval to 1 hour 2018-12-07 20:35:00 -08:00
Calvin Montgomery
aa2348656d Implement self-service account deletion 2018-12-07 20:35:00 -08:00
Calvin Montgomery
37c6fa3f79 Fix eslint complaint 2018-11-16 19:55:58 -08:00
Calvin Montgomery
fe4030a247 Fix ustream link parsing 2018-11-16 19:52:09 -08:00
Calvin Montgomery
4c9e85b293 Support IO token bucket capacity > refill rate 2018-11-15 23:04:03 -08:00
Calvin Montgomery
8456b6a125 Implement #767 for custom media only 2018-11-15 22:52:04 -08:00
Calvin Montgomery
027b27c1b0 Bump max message length (#782) 2018-11-15 22:48:30 -08:00
Calvin Montgomery
3620b07816 Add userlist-ignored strikethrough for ignored users 2018-11-15 22:44:21 -08:00
Calvin Montgomery
8c9622f1b2 Fix #783 2018-11-15 22:40:01 -08:00
Calvin Montgomery
4ccdca6dca Default channel-storage to database for new installs 2018-11-14 22:48:49 -08:00
Calvin Montgomery
bfff2900ca deps: replace cytubefilters short hash with full hash 2018-11-14 22:03:14 -08:00
Calvin Montgomery
b85406716b deps: bump cytubefilters git hash 2018-11-14 22:01:38 -08:00
Calvin Montgomery
f7cc00d16b Fix tabcomplete sort bug exposed by new v8 2018-11-12 21:01:43 -08:00
Calvin Montgomery
a9fac9d6d0 Add node.js 11.x to .travis.yml 2018-11-12 21:01:43 -08:00
Calvin Montgomery
cd94c8b83d Use page visibility API instead of buggy window.focus tracking 2018-11-11 20:24:19 -08:00
Calvin Montgomery
60a39890f0 Fix hostname comparison in /login 2018-11-11 16:11:51 -08:00
Calvin Montgomery
2d6af31c00 voteskip: add early exit for duplicate votes 2018-11-11 16:08:00 -08:00
Calvin Montgomery
f6a58d00b2 Adjust some socket.io settings (#780) 2018-11-07 21:23:00 -08:00
Calvin Montgomery
1f28c0b87d Add checks for kisscartoon/kissanime/mega to direct the user to the FAQ 2018-10-21 22:26:43 -07:00
Calvin Montgomery
801d3d9be1 Fix #775 2018-10-21 22:18:22 -07:00
Calvin Montgomery
5b86fb3187 Implement #779 2018-10-21 22:12:49 -07:00
really-need-an-api-key
0bc866dbfa Add desktop notifications 2018-10-12 20:19:45 -07:00
Calvin Montgomery
1923af16a9 Fix a few minor error conditions 2018-09-30 21:22:20 -07:00
Calvin Montgomery
ce44bfea9e Be stricter about ustream IDs 2018-09-30 21:05:04 -07:00
Calvin Montgomery
24a13c12cf Minor fixes, logging, metrics 2018-09-30 21:03:09 -07:00
Calvin Montgomery
13585a5e6a Replace raw DDL with knex table builder 2018-09-30 20:43:45 -07:00
Calvin Montgomery
0c100b1dec
Remove "experimental" note from custom-media.md 2018-09-24 21:20:50 -07:00
Calvin Montgomery
c7fcd11e53 Fix channel save error introduced by removing async-to-generator 2018-08-29 20:59:07 -07:00
Calvin Montgomery
d9e2a62f77 Add check for #766 2018-08-29 20:40:24 -07:00
Calvin Montgomery
7b0427afa2 Remove code that was never finished and likely won't be used 2018-08-27 22:07:42 -07:00
Calvin Montgomery
553052f901 Drop node.js 6.x 2018-08-27 21:59:48 -07:00
Calvin Montgomery
c90d9c0ddc Remove flow (not being used) 2018-08-27 21:48:52 -07:00
Calvin Montgomery
0bd11c3bba Add forgotten file 2018-08-26 22:08:59 -07:00
Calvin Montgomery
db48104b80 Initial mixer implementation 2018-08-26 22:04:14 -07:00
Calvin Montgomery
f19efdb859 Fix #762 2018-08-18 13:14:37 -07:00
Calvin Montgomery
c5c4fba7ce Fix unused import 2018-08-18 12:28:18 -07:00
Calvin Montgomery
a9a644460f Fix #760 2018-08-18 12:27:24 -07:00
Calvin Montgomery
cb687fc078 zalgo text hasn't been funny for years, guys 2018-08-06 19:56:55 -07:00
Calvin Montgomery
d54707c9c7 Wrap raw file HEAD check in try-catch in case of invalid URL 2018-07-25 21:38:09 -07:00
Calvin Montgomery
3d520ecf57 Add ffmpeg error handlers for 405 and 501 2018-07-25 21:34:02 -07:00
Calvin Montgomery
878b30bdb2 Fix undefined dereference in rank callback 2018-07-25 21:27:28 -07:00
Calvin Montgomery
67b1c97d89 Add io.throttle-in-rate-limit for socket event rate 2018-07-25 21:07:07 -07:00
Calvin Montgomery
db2361aee9 Misc fixes for password reset
* Remove messaging about asking an administrator for help if no email
    is associated with the account (no longer correct or relevant)
  * Compare user-provided email with registered email case-insensitively
    (#755)
  * Replace antiquated hash generator with cryptographically secure
    random byte string generator
2018-07-11 19:21:32 -07:00
Calvin Montgomery
3db751b65f Fix socket count metric leak 2018-07-09 20:24:53 -07:00
Ryan Huang
7acae30875 Add breaking spaces into footer 2018-07-09 17:15:20 -07:00
Calvin Montgomery
aca40dde0c Add note about unsupported filetypes 2018-06-15 20:33:55 -07:00
Calvin Montgomery
dd23564c15 link-domain-blacklist: fix blank blacklist matching empty string 2018-06-14 18:45:35 -07:00
Calvin Montgomery
fa49921866 Speed up join by avoiding quadratic userlist code
At some point the entire user presence logic needs to be refactored for
efficiency, but this at least gives a huge reduction in first page load
time for large channels.
2018-06-06 22:47:00 -07:00
Calvin Montgomery
3413c3bdaa Reject guest names matching the reserved usernames regex 2018-06-03 22:01:40 -07:00
Calvin Montgomery
90b5e5e09f deps: bump mocha and babel 2018-06-03 21:55:41 -07:00
Calvin Montgomery
125a781cc7 deps: updates to support node.js 10 2018-06-03 21:19:12 -07:00
Xaekai
a632a4cafa Show poll creator on hover of timestamp 2018-05-28 15:17:27 -07:00
Calvin Montgomery
fdab26b792 Hoist sortUserlist outside of userlist population inner loop 2018-05-26 13:28:26 -07:00
Calvin Montgomery
bfe0d75278 Add check for error condition exposed by misbehaving bot 2018-04-08 19:19:22 -07:00
Calvin Montgomery
976b0a2168 Fix error introduced by lint changes 2018-04-08 19:17:03 -07:00
Calvin Montgomery
e9a183bf9a Replace muted user set implementation with ES6 Set 2018-04-08 19:11:54 -07:00
Calvin Montgomery
62417f7fb8
Add eslint (#741) 2018-04-07 15:30:30 -07:00
Adam davis
953428cad5 Add Admin Setting - Block anonymous connections (#740) 2018-04-07 11:24:52 -07:00
Calvin Montgomery
ef7bf1a319 Use path.join(__dirname, ...) to potentially avoid www/js/player.js issue 2018-04-05 20:48:59 -07:00
Calvin Montgomery
c1e78fd4dc Kill process if www/js/player.js is not found at startup 2018-04-05 20:39:49 -07:00
Calvin Montgomery
2087921072 Clarify some wording 2018-03-17 17:47:27 -07:00
Calvin Montgomery
6070f7fc06 Update the README 2018-03-17 17:46:21 -07:00
Calvin Montgomery
304a6c9cfa Fix parseMediaLink stripping querystring from HLS manifest URLs 2018-03-17 10:49:08 -07:00
Calvin Montgomery
34eaca7b84 Fix version check in index.js to reject node <v6 2018-03-13 22:36:21 -07:00
Calvin Montgomery
fcfc45dd70 Save YouTube playlists to library in batch to avoid connection pool starvation 2018-03-05 22:19:51 -08:00
Calvin Montgomery
54bf7f1c5b Strip GDrive metadata from saved channel playlists 2018-03-05 21:56:08 -08:00
Calvin Montgomery
8340bf2c81 Add notice that quality preference doesn't work for YouTube anymore (#726) 2018-03-05 21:51:40 -08:00
Calvin Montgomery
81e1947656 Clear template cache on /reload (#734) 2018-03-05 21:46:58 -08:00
Calvin Montgomery
247cf770d0 Avoid O(N^2) loop when loading channel emotes on channel load 2018-03-05 21:35:56 -08:00
Calvin Montgomery
726a5bf7c4 Minor tweaks to specific error conditions 2018-02-24 19:51:28 -08:00
Calvin Montgomery
79556d9365
deps: remove "q" (#731)
Insert Star Trek joke here.

Also did significant refactoring of the surrounding logic for the things
that depended on Q.
2018-02-24 19:47:50 -08:00
Calvin Montgomery
d5f5c91b05 Minor fixes 2018-02-15 19:59:23 -08:00
Calvin Montgomery
49661a95ab Upgrade dependencies 2018-02-15 19:58:33 -08:00
Calvin Montgomery
03f30a82b9 Fix botched version bump in package.json 2018-02-01 17:41:06 -08:00
Calvin Montgomery
966da1ac58 Revert "Replace quadratic emote list impl with Map"
This reverts commit 0f9bc44925.

The original commit was not backwards compatible with use cases that
users were relying on, such as emotes being sorted in insertion order by
default.

I will develop a new patch which fixes the performance issue in a
backwards compatible way.
2018-02-01 17:39:45 -08:00
Calvin Montgomery
aeab31825e Fix a raw file error caused by facebook CDN violating RFC 2616 2018-01-21 18:53:16 -08:00
Calvin Montgomery
e7781b5c09 Remove accidentally committed script 2018-01-18 19:48:32 -08:00
Calvin Montgomery
8821de0e7d Try to reduce the extra crap logged when a db query fails 2018-01-18 19:47:55 -08:00
Calvin Montgomery
0f9bc44925 Replace quadratic emote list impl with Map 2018-01-18 19:34:57 -08:00
Calvin Montgomery
8399eab33f Fix error on invalid regex for /clean 2018-01-17 21:54:43 -08:00
Calvin Montgomery
326e67893c Minor bugfix 2018-01-14 15:43:12 -08:00
Calvin Montgomery
1797e11b43 Sanitize google drive IDs to remove URL hash etc. 2018-01-14 15:15:59 -08:00
Calvin Montgomery
46a738b7f4 Minor tweak to playlist dirty check 2018-01-14 15:08:55 -08:00
Calvin Montgomery
d706bf63b1 Fix ustream 2018-01-14 15:02:15 -08:00
Calvin Montgomery
fec1372e4e Annual bit flip 2018-01-07 15:45:18 -08:00
Calvin Montgomery
c07cf7c13a Remove confirmation prompt from postinstall 2018-01-07 15:42:08 -08:00
Calvin Montgomery
e350eb731b Fix #728 2018-01-07 15:14:20 -08:00
Calvin Montgomery
cf9b95a265 Add experimental support for dash streaming 2018-01-06 11:00:59 -08:00
Calvin Montgomery
67fbc8e267 Add more information to the voteskip passed log message 2018-01-06 10:31:59 -08:00
Calvin Montgomery
4b48966e1d Add ffprobe errors for ECONNRESET and CERT_HAS_EXPIRED 2018-01-06 10:13:07 -08:00
Calvin Montgomery
6d0498987a Add sanity check for one instance of error unload
Unfortunately I think this is just one of a whole class of race
conditions caused by errored channels being unloaded immediately without
waiting for the refcounter to reach 0.

However, this one is the only one that appears commonly in the logs so
adding this check should buy time to rethink the overall problem.
2018-01-06 10:09:22 -08:00
Calvin Montgomery
78bffad888 Fix errored channels getting stuck during unload 2018-01-06 09:59:18 -08:00
Zynjec
7b328b10c3 Removed Vidme provider from getInfo
Loading a channel fails due to it being removed.

`Uncaught exception: Error: Cannot find module 'cytube-mediaquery/lib/provider/vidme'`
2017-12-27 18:15:21 -08:00
Calvin Montgomery
8a8532fc84 Add node.js 9 to .travis.yml 2017-12-27 14:24:57 -08:00
Calvin Montgomery
95e147b5a0 Use socket.handshake instead of socket.client.request
Fixes a bug where sockets would be rejected if they connected directly
with the 'websocket' transport instead of doing an AJAX connection with
websocket upgrade (e.g. if `transports: ['websocket']` is passed to the
socket.io-client constructor).

See https://github.com/socketio/socket.io/blob/master/docs/API.md#sockethandshake
2017-12-27 14:24:33 -08:00
Calvin Montgomery
0b6106a89e Clarify gdrive userscript error when fmt_stream_map is missing 2017-12-26 20:40:12 -08:00
Calvin Montgomery
fa74ee0538 Add NEWS.md update I forgot to commit 2017-12-26 11:04:40 -08:00
Calvin Montgomery
24322d3b52 Remove config option that is no longer used 2017-12-26 11:00:18 -08:00
Calvin Montgomery
b7bc93f194 Disable vid.me (RIP) 2017-12-24 11:19:30 -08:00
Calvin Montgomery
0c330a82ce Add dirty check to playlist for efficiency of channel saving 2017-12-16 10:34:04 -08:00
Calvin Montgomery
a4e72a002a Fix #719 2017-12-16 00:05:28 -08:00
Calvin Montgomery
7fbd62142e Minor tweaks/fixes 2017-12-15 19:10:32 -08:00
Calvin Montgomery
29be9233e9 Add check for weird setAFK edge case 2017-12-11 22:46:41 -08:00
Calvin Montgomery
1e969117c4 Fix #722 2017-12-10 19:28:05 -08:00
Calvin Montgomery
fbee6d2ab7 Fix a few common causes of error logs (incl. better ffprobe error messages) 2017-12-10 16:39:06 -08:00
Calvin Montgomery
c4cc22dd05 Add experimental feature to reduce database writes for channel data 2017-12-10 10:36:28 -08:00
Calvin Montgomery
a9062159ed Fix partial saving for flatfile channel data 2017-12-10 09:48:40 -08:00
Calvin Montgomery
64350cc492 Disable test for middleware commented out due to #724 2017-12-06 22:17:37 -08:00
Calvin Montgomery
4e8c97bfb5 Fix deprecation warning about no callback to fs.unlink 2017-12-06 22:16:25 -08:00
Calvin Montgomery
39587a8448 Add DB query error count metric 2017-12-06 22:13:07 -08:00
Calvin Montgomery
9886f648f2 Workaround for #724 2017-12-06 22:10:06 -08:00
Calvin Montgomery
60f77d4eb9
Merge pull request #720 from calzoneman/servicelogin
This resolves an issue where Google returns HTTP200 but provides an H…
2017-11-28 21:37:28 -08:00
Xaekai
6a0608bf7e Explicit type conversion in version number comparison. 2017-11-27 23:56:21 -08:00
Xaekai
aa5066762b This resolves an issue where Google returns HTTP200 but provides an HTML redirect to a login portal instead of video data.
Closes #718
2017-11-27 23:37:41 -08:00
Calvin Montgomery
342e5d406a Drive userscript: support violentmonkey (#713) 2017-11-27 22:42:50 -08:00
Calvin Montgomery
a260f79c7d Replace gm4 polyfill 2017-11-20 18:00:45 -08:00
Calvin Montgomery
85169fbb56 Update drive userscript (#714) 2017-11-15 22:27:31 -08:00
Calvin Montgomery
875337d9a6 web/account: add referrer check 2017-11-05 16:17:37 -08:00
Calvin Montgomery
b876c8907a ffmpeg: preserve cookies when following redirects in pre-flight req 2017-11-05 16:01:39 -08:00
Calvin Montgomery
b453aecee5 Replace froogaloop
Froogaloop no longer appears to work.

Followed migration guide: https://github.com/vimeo/player.js/blob/master/docs/migrate-from-froogaloop.md
2017-10-28 23:10:15 -07:00
Calvin Montgomery
3cd8bfa8c7 Remove /sioconfig for real 2017-09-30 15:26:47 -07:00
Calvin Montgomery
a2be65aead Reset prometheus summaries for more accurate percentiles per 5 minutes 2017-09-27 21:55:42 -07:00
Calvin Montgomery
014f3f008e Remove config key that is no longer used 2017-09-27 21:50:51 -07:00
Calvin Montgomery
c4ad9099c2 Merge pull request #707 from calzoneman/nodemailer-upgrade
Upgrade nodemailer to 4.x
2017-09-27 21:46:54 -07:00
Calvin Montgomery
f975f7ef85 Update password reset to use new nodemailer impl 2017-09-26 21:22:15 -07:00
Calvin Montgomery
9cfe71d4c4 Start working on nodemailer upgrade 2017-09-25 22:31:45 -07:00
Calvin Montgomery
071def0838 Fix streamable autoplay 2017-09-25 19:25:31 -07:00
Calvin Montgomery
8db22ad924 Implement playerjs for streamable (#706) 2017-09-25 19:18:46 -07:00
Calvin Montgomery
bfc7cfc193 Remove old /useragreement 2017-09-19 22:07:00 -07:00
Calvin Montgomery
9868a97dbd Remove a couple config keys that are no longer used 2017-09-19 22:03:34 -07:00
Calvin Montgomery
c159fa8060 Remove old HTTPS redirect kludges 2017-09-19 20:49:33 -07:00
Calvin Montgomery
4e1bce6a24 Remove flaky (in CI) test 2017-09-19 19:11:18 -07:00
Calvin Montgomery
de5cc3352a Fix another bug with prometheus socket.io emtrics 2017-09-19 19:03:43 -07:00
Calvin Montgomery
9a1d50dcd3 Add support for v8-profiler (optional dep) 2017-09-18 21:54:36 -07:00
Calvin Montgomery
4db78deda3 Support updating profile via /account/data 2017-09-06 22:53:34 -07:00
Calvin Montgomery
9e3426633d Support updating email via /account/data 2017-09-05 23:11:28 -07:00
Calvin Montgomery
5b6f86668a Refactoring 2017-09-05 22:47:29 -07:00
Calvin Montgomery
3eb97bab6a Fix bug in cytube_sockets_num_connected metric 2017-09-04 10:04:33 -07:00
Calvin Montgomery
97231e515c player: support HLS vod for vidme (fixes #703)
- Upgrade videojs-contrib-hls to latest version
  - Update VideoJSPlayer to support "auto" quality tag to delegate to
    the HLS plugin for automatic quality selection
  - mediaquery change:
    9f5122e031
2017-09-04 09:44:30 -07:00
Calvin Montgomery
45d0e0b4c3 Guard unfinished web route with env variable 2017-09-03 17:22:57 -07:00
Calvin Montgomery
b76869e2d2 Add some basic tests for implemented /account/data handlers 2017-09-01 21:20:07 -07:00
Calvin Montgomery
8b1b501bbd Start working on /account/data controller 2017-08-30 22:45:48 -07:00
Calvin Montgomery
33b2bc2d30 Add basic knex methods for channel data needed for /account/* 2017-08-29 21:23:04 -07:00
Calvin Montgomery
269aa6bfe6 Add basic knex methods to be used for /account/* pages 2017-08-28 23:37:32 -07:00
Calvin Montgomery
162f8fd9b5 Fix index page JS submit 2017-08-24 21:01:10 -07:00
Calvin Montgomery
3d50b8f52e Fix getSafeReferrer when referrer is null 2017-08-24 20:55:18 -07:00
Calvin Montgomery
cc69b3c225 Revert "Remove legacy /sioconfig and user agreement link"
ACP has a dependency on `/sioconfig`.  Reverting until that can be
fixed.

This reverts commit a48cab81b9.
2017-08-23 23:15:30 -07:00
Calvin Montgomery
cacde7f72d Fix unhandled rejections in webserver 2017-08-23 23:02:08 -07:00
Calvin Montgomery
712a8c228b Refactor most pug templates to share a common layout template 2017-08-22 22:09:48 -07:00
Calvin Montgomery
0810591fe3 Remove unnecessary template mixin vars 2017-08-22 17:33:29 -07:00
Calvin Montgomery
7e6312f9d1 Remove ?dest= redirect logic for /login and use referrer instead 2017-08-22 17:25:18 -07:00
Calvin Montgomery
a48cab81b9 Remove legacy /sioconfig and user agreement link
- `/sioconfig` has been deprecated for ages in favor of
    `/socketconfig/${channel}.json`
  - Each website administrator should be responsible for determining the
    appropriate terms of service for their website instead of CyTube
    providing a default one.
2017-08-21 23:19:19 -07:00
Calvin Montgomery
7c897d91db Add crossorigin attribute for custom media with text tracks
Mitigates #702
2017-08-21 20:06:07 -07:00
Calvin Montgomery
0885a619b9 Generate .meta.js for gdrive userscript for update checks
Tampermonkey automatically requests www/js/cytube-google-drive.meta.js
to check for updates.  Changed the userscript generator to write an
additional .meta.js file so that works instead of 404ing.
2017-08-19 16:31:02 -07:00
Calvin Montgomery
9f0444a962 Fix jquery 404 on /register 2017-08-19 16:13:15 -07:00
Calvin Montgomery
ae7098085c Work on knexifying password resets 2017-08-16 23:28:29 -07:00
Calvin Montgomery
791a712a68 Move channel register/delete reload logic to message bus 2017-08-15 18:55:36 -07:00
Calvin Montgomery
d16cfb7328 Add message bus for #677 2017-08-15 18:23:03 -07:00
Calvin Montgomery
9ee650461f Change unhandledRejection from fatal log level to error 2017-08-14 20:35:30 -07:00
Calvin Montgomery
2990d83c02 ffmpeg: add ETIMEDOUT error message 2017-08-14 20:33:09 -07:00
Calvin Montgomery
99076412b6 Fix unhandled rejection 2017-08-14 20:31:45 -07:00
Calvin Montgomery
c6c3bafca2 database: include legacy query() in prometheus metrics 2017-08-14 18:24:53 -07:00
Calvin Montgomery
82004aab73 ioserver: change on disconnect to once to avoid double-counting 2017-08-14 18:23:07 -07:00
Calvin Montgomery
82bd645781 Minor cleanup of some no longer used client stuff 2017-08-13 22:33:54 -07:00
Calvin Montgomery
70b875c0e9 Remove some ancient db upgrade stuff
If anyone is still running a database from 2014 they want to upgrade,
sorry.
2017-08-13 22:19:47 -07:00
Calvin Montgomery
4102d6eaf2 Refactor index.js logic into src/main 2017-08-13 22:16:42 -07:00
Calvin Montgomery
ba8088b678 videojs: default quality to 480 instead of 1080 2017-08-13 21:48:50 -07:00
Calvin Montgomery
a90d88ad65 Fix race condition that might be causing errors 2017-08-12 13:30:24 -07:00
Calvin Montgomery
8a8ed0a932 ffmpeg: better error messages for ECONNREFUSED and ENOTFOUND 2017-08-12 13:20:44 -07:00
Calvin Montgomery
d0c1e8cbd9 Change metric names to follow prometheus naming guide 2017-08-12 13:12:58 -07:00
Calvin Montgomery
92f0a956b9 custom-media: import spec and fix a minor missed validation 2017-08-08 20:46:10 -07:00
Calvin Montgomery
04c9d48779 custom-media: implement queueing and playback changes 2017-08-08 20:35:17 -07:00
Calvin Montgomery
a6de8731b3 custom-media: add metadata downloader 2017-08-07 22:37:56 -07:00
Calvin Montgomery
f4ce2fe69d custom-media: add converter to CyTube Media object 2017-08-07 21:44:55 -07:00
Calvin Montgomery
8b7cdfd4c3 soundcloud: fix getVolume to match setVolume 2017-08-07 21:08:04 -07:00
Calvin Montgomery
c7f7dcfed3 custom-media: use url.parse, not whatwg URL (node v6 compat) 2017-08-06 21:59:14 -07:00
Calvin Montgomery
ea6e3f921f custom-media: add validator
Initial work for #655
2017-08-06 21:50:27 -07:00
Calvin Montgomery
331a4626a0 Fix borrow-rank 2017-08-06 20:42:33 -07:00
Calvin Montgomery
0b560f15a9 Add prometheus counter for changeMedia 2017-08-05 18:50:27 -07:00
Calvin Montgomery
dac2e41488 Fix and enable efficient emotes by default 2017-08-05 12:22:58 -07:00
Calvin Montgomery
cb6cfc8455 Instrument some more metrics with prometheus 2017-08-02 21:24:44 -07:00
Calvin Montgomery
6043647cb7 Skip full user auth for most page renders
Previously, the user's session cookie was being checked against the
database for all non-static requests.  However, this is not really
needed and wastes resources (and is slow).

For most page views (e.g. index, channel page), just parsing the value
of the cookie is sufficient:

  * The cookies are already HMAC signed, so tampering with them ought to
    be for all reasonable purposes, impossible.
  * Assuming the worst case, all a nefarious user could manage to do is
    change the text of the "Welcome, {user}" and cause a (non-functional)
    ACP link to appear clientside, both of which are already possible by
    using the Inspect Element tool.

For authenticated pages (currently, the ACP, and anything under
/account/), the full database check is still performed (for now).
2017-08-01 21:40:26 -07:00
Calvin Montgomery
0118a6fb15 Refactor socket.io controller 2017-08-01 19:29:11 -07:00
Calvin Montgomery
107155a661 Stop knex from thrashing idle connections 2017-07-27 18:01:40 -07:00
Calvin Montgomery
7bd9934e58 Minor cleanup of no longer used things 2017-07-26 20:32:51 -07:00
Calvin Montgomery
f593f7283c Replace alert() with modal for ACP password reset
Some browsers (e.g. Chrome) don't allow copying text out of alert()
dialogs.
2017-07-24 22:35:15 -07:00
Calvin Montgomery
5a78056c91 Some small refactoring 2017-07-24 22:08:26 -07:00
Calvin Montgomery
e80613c7ec Fix rtmp again because chrome is picky about mime types 2017-07-23 17:55:25 -07:00
Calvin Montgomery
9dd0ee4446 Fix logger misreference in copied-over lualoader 2017-07-22 11:44:33 -07:00
Calvin Montgomery
08a42f6739 ffmpeg: add specific error for invalid SSL cert 2017-07-22 11:32:43 -07:00
Calvin Montgomery
282ad986b6 Deprecate legacy vimeo-oauth lookup 2017-07-22 11:14:29 -07:00
Calvin Montgomery
52030506b5 deps: remove status-message-polyfill
This hasn't been necessary since node v0.10, and CyTube only supports
node v6.x+ by this point.
2017-07-22 10:45:36 -07:00
Calvin Montgomery
a8f1e48157 ffmpeg: remove bitrate and codec warning
Browsers which don't support CyTube's limited subset of
generally-supported codecs probably aren't worth warning about.

1Mbps is way too low of a threshold to warn about bandwidth, but even if
the threshold for warning were raised, it's probably still not that
useful.
2017-07-22 10:43:18 -07:00
Calvin Montgomery
ffde151ebd Make redis announcement channel configurable
Finally fix the bug where announcements bleed across beta & live due to
sharing a redis pubsub channel.
2017-07-22 10:41:22 -07:00
Calvin Montgomery
964feb7243 Add id field to announcements and hide previously closed announcements 2017-07-22 10:35:45 -07:00
Calvin Montgomery
ff3ececc36 Copy utils from cytube-common and remove dep
The `cytube-common` module was created as part of a now-defunct
experiment and since then has just remained a crufty container for a few
utils.  Moved the utils to the main repo and removed the dependency.
2017-07-19 20:47:02 -07:00
Calvin Montgomery
e780e7dadb Deprecate stats table in favor of prometheus integration 2017-07-17 21:58:58 -07:00
Calvin Montgomery
c7bec6251e Begin prometheus integration
Add a dependency on `prom-client` and emit a basic latency metric for
testing purposes.  Add a new configuration file for enabling/disabling
prometheus exporter and configuring the listen address.
2017-07-16 22:35:33 -07:00
Calvin Montgomery
dd770137e5 Fix error for rtmp player 2017-07-15 20:17:13 -07:00
Calvin Montgomery
7efa3d4704 deps: upgrade to socket.io 2.0 2017-07-15 14:56:36 -07:00
Calvin Montgomery
d9813e6244 Remove legacy tab complete (no longer used) 2017-07-15 14:48:53 -07:00
Calvin Montgomery
c152a19624 Ignore library cached metadata when queueing
The use of the channel library as a cache for metadata to avoid
re-requesting metadata for known media is an optimization that dates
back to 1.0.  However, it doesn't have any TTL, is prone to bugs, and is
of dubious value.

This commit ignores the results of the library check when queueing a new
video, opting to always re-request the metadata.  This fixes a few bugs:

  * Google Drive metadata being lost when storing in library
  * Streamable metadata being lost when storing in library
  * Videos in the channel library that are now unavailable on their
    source website being queueable and then failing to play (e.g. deleted
    YouTube videos).

In its place, a small fail-open check is left behind to emit metric
counters on how many queues would have been cache-hits, to provide
insight into whether a proper caching solution (i.e. one not tacked on
top of the library) would be worth pursuing or not.  This will be
removed eventually.
2017-07-15 14:41:37 -07:00
Calvin Montgomery
b7ceee8ef4 Fix video sources being lost when playlist is saved 2017-07-15 14:12:32 -07:00
Calvin Montgomery
30a5657d62 soundcloud: fix volume issue
It took them 4 years, but they finally did actually make the player
accept volume in the range 0-100 like their documentation suggests.

*slow clap*
2017-07-10 21:38:27 -07:00
Calvin Montgomery
fc66e758ac Minor fix 2017-07-09 22:40:09 -07:00
Calvin Montgomery
637bcad816 camo: include subdomains of whitelisted domains in whitelist 2017-07-08 20:46:42 -07:00
Calvin Montgomery
07179d6c83 Upgrade to jsli 2.0 2017-07-08 20:11:54 -07:00
Calvin Montgomery
486ce04a3e camo: support URL encoding option 2017-07-08 19:21:14 -07:00
Calvin Montgomery
54045766f2 Replace instances of cytube-common logger with jsli 2017-07-02 22:38:54 -07:00
Calvin Montgomery
00901f9cdb Remove junk from an old abandoned project 2017-07-02 22:35:12 -07:00
Calvin Montgomery
860775a90b Remove html5hack (legacy google drive setting) 2017-07-02 22:30:19 -07:00
Calvin Montgomery
5500054b84 Add resolution switcher plugin for video.js
Allows switching resolutions via the video.js UI.  Also added support on
the player side for 540p, 1440p, and 2160p videos, although the metadata
extractors have not been updated to provide these sources yet.
2017-07-01 16:54:19 -07:00
Calvin Montgomery
d36bc160ca Merge pull request #693 from Xaekai/damnit
Minor fixes to afk stuff.
2017-06-29 21:09:54 -07:00
Xaekai
18bf1b946b Minor fixes to afk stuff. 2017-06-29 19:04:49 -07:00
Calvin Montgomery
7ebf3c18ab Add knex AliasesDB 2017-06-28 22:58:40 -07:00
Calvin Montgomery
76e0d1b7ec Use proxy-addr for parsing x-forwarded-for
Closes #683 by providing functionality to trust proxies other than
localhost.
2017-06-27 23:37:18 -07:00
Calvin Montgomery
9cffd7dde8 Merge pull request #691 from calzoneman/upgrade-babel-nodejs-6
Upgrade babel preset for node 6, add async transform
2017-06-21 22:34:46 -07:00
Calvin Montgomery
2427b3ef4b Merge pull request #690 from Xaekai/shadow.anons
Send shadowmuted messages to anons
2017-06-20 23:21:27 -07:00
Calvin Montgomery
9fc399c200 Upgrade babel preset for node 6, add async transform 2017-06-20 23:16:33 -07:00
Xaekai
5f71c4d368 Send shadowmuted messages to anons
Resolves #689
2017-06-20 22:29:27 -07:00
Calvin Montgomery
a96f7976d8 Change Tor exit list
Use the endpoint suggested in #688 to avoid unnecessarilly banning
relays.
2017-06-17 10:12:15 -07:00
Calvin Montgomery
6161f4ad44 Add ffmpeg error log for request failure case 2017-06-17 09:47:22 -07:00
Calvin Montgomery
6633e23aa3 Add characterization test for sanitize-html
At various times in the past, upgrades in the sanitize-html library that
changed behavior of HTML filtering have caused things like emotes to
break unexpectedly.  This commit adds a basic test to sanitize
non-alphanumeric characters found in channels' emote codes so that if
the library changes, the test will break and give a heads up that
something changed.
2017-06-17 09:47:22 -07:00
Calvin Montgomery
53cee986c6 Resend userlist if rank changes meta visibility
Fixes #681.  Technically, resending the entire userlist is not
necessary; it would be sufficient to resent setUserMeta, but there's not
currently a bulk frame for that so sending the userlist is probably more
efficient.
2017-06-17 09:47:22 -07:00
Calvin Montgomery
efae9c4774 Merge pull request #686 from Xaekai/silly.bug
Fix a typo.
2017-06-16 22:00:02 -07:00
Xaekai
be8318f014 Fix a typo. 2017-06-16 21:50:17 -07:00
Calvin Montgomery
33f632036e Merge pull request #684 from Xaekai/afk.meta
Single source of truth for AFK
2017-06-16 21:42:23 -07:00
Xaekai
2dc6504a77 Use a consistent pattern. 2017-06-16 21:37:30 -07:00
Calvin Montgomery
0f5193c700 Merge pull request #685 from Xaekai/custom.path
Customize channel path
2017-06-16 21:22:11 -07:00
Xaekai
6d4558c978 Allow channel path to be customizable
We now allow server operators to customize the /r/ part of the channel links
The new config option in the template is commented and the config module validates and will terminate with status 78 if an improper value is used.
We've also dropped some old cruft and uses a more elegant method to assign CHANNEL.name

Resolves #668
2017-06-16 20:09:36 -07:00
Xaekai
f89832a6d1 Gracefully allow script authors time to update their code 2017-06-15 22:09:09 -07:00
Xaekai
df0fc769d9 Single source of truth for AFK
Resolves #678
2017-06-15 21:48:17 -07:00
Calvin Montgomery
00a65a1584 Deprecate legacy global ban junk 2017-06-05 23:18:20 -07:00
Calvin Montgomery
b23a858a8c Integrate socket.io ban check with GlobalBanDB 2017-06-05 23:14:45 -07:00
Calvin Montgomery
ed811db6ec Integrate ACP with GlobalBanDB class 2017-06-05 22:53:35 -07:00
Calvin Montgomery
b80a87ba01 Add integration test for global bans 2017-06-05 22:45:14 -07:00
Calvin Montgomery
8ad9b4e543 Remove redundant template local 2017-06-05 18:57:04 -07:00
Calvin Montgomery
58c65a5bac Use host header instead of req.host which is actually req.hostname 2017-06-05 18:53:36 -07:00
Calvin Montgomery
830486bc4f Fix channel registration error due to extra knex query param 2017-06-05 18:46:41 -07:00
Calvin Montgomery
07c801a12d Merge pull request #674 from Xaekai/channel.registration
Touch up validations on account pages
2017-06-05 18:40:50 -07:00
Xaekai
699aa2abe1 Finish validation touchup 2017-06-05 00:06:15 -07:00
Xaekai
d42de93d74 Round 2. 2017-06-04 22:16:40 -07:00
Xaekai
668477d711 Nice is subjective. 2017-06-04 22:04:39 -07:00
Xaekai
8769ca1dd9 Basic validation of channel IDs on the registration page 2017-06-04 19:03:42 -07:00
Calvin Montgomery
d0712d007e Work on refactoring global IP ban database calls 2017-05-31 22:46:15 -07:00
Calvin Montgomery
7fcf31dec6 Merge pull request #671 from calzoneman/knex
The knexening: part 1
2017-05-29 13:16:35 -07:00
Calvin Montgomery
290f802b7c Merge pull request #670 from calzoneman/camo-proxy-chat-images
Support proxying chat images via camo
2017-05-29 10:32:01 -07:00
Calvin Montgomery
e02bc46ed2 Add camo example config 2017-05-29 10:24:49 -07:00
Calvin Montgomery
2a694e73af The knexening: part 1 2017-05-28 22:39:27 -07:00
Calvin Montgomery
22a9acfc90 Support proxying chat images via camo
Camo: https://github.com/atmos/camo.  This has a couple advantages over
just allowing images to be dumped as-is:

  - Prevents mixed-content warnings by allowing the server to proxy HTTP
    images to an HTTPS camo instance
  - Protects users' privacy by not exposing their browser directly to
    the image host
  - Allows the camo proxy to intercept and reject bad image sources
    (URLs that are not actually images, gigapixel-sized images likely to
    DoS users' browsers, etc.)

Whitelisting specific domains is supported for cases where the source is
known to be trustworthy.
2017-05-28 19:38:43 -07:00
Calvin Montgomery
f968521936 Remove google drive refresh logic
No longer relevant since the video links are retrieved by the
userscript.
2017-05-28 18:35:13 -07:00
Calvin Montgomery
e9c519c6e2 Add twitch clip support (#659) 2017-05-27 11:49:43 -07:00
Calvin Montgomery
995ab142e3 Merge pull request #669 from Xaekai/hail.satan
Glory to the dark lord
2017-05-25 21:42:05 -07:00
Xaekai
5163c2acb1 Glory to the dark lord 2017-05-25 08:04:19 -07:00
Calvin Montgomery
97f94dd3ac Merge pull request #667 from Xaekai/acp.users
[ACP] Allow searching users by email.
2017-05-24 19:50:05 -07:00
Xaekai
1d65eb036e Use a more salient variable name.
Use a style thats readable with brevity.
2017-05-24 19:18:47 -07:00
Xaekai
93ef067b8c [ACP] Allow searching users by email. 2017-05-24 04:44:55 -07:00
Calvin Montgomery
d23b5278b1 Rename Hitbox -> Smashcast 2017-05-20 16:50:00 -07:00
Calvin Montgomery
55b03d51d7 Fix setOptions for playlist_max_duration_per_user 2017-05-20 16:31:52 -07:00
Calvin Montgomery
02587dbb5c Merge pull request #664 from Xaekai/emote.rename
Emote renaming
2017-05-18 22:02:50 -07:00
Xaekai
9cfd97088e Some validation 2017-05-18 20:12:00 -07:00
Xaekai
8434d20826 Fix minor issues with emote rename 2017-05-17 09:50:47 -07:00
Xaekai
8e3ce4e1c3 Emote renaming
This allow emotes to be renamed in the same fashion the image URLs can be changed.
2017-05-16 10:08:53 -07:00
Calvin Montgomery
5f6176b18c Merge pull request #663 from Xaekai/vidme.parseupdate
Add support for embedded VidMe URLs
2017-05-15 22:01:57 -07:00
Xaekai
929e1b2c69 Add support for embedded VidMe URLs 2017-05-14 19:21:01 -07:00
Calvin Montgomery
dd97c244f2 Merge pull request #662 from Xaekai/SignalHandler
Add a simple listener for SIGUSR2 to reload SSL certs
2017-05-14 18:39:50 -07:00
Calvin Montgomery
071170dd90 Merge pull request #661 from Xaekai/ServcmdCert
Add tab completion entry for reloading cert
2017-05-14 18:39:12 -07:00
Calvin Montgomery
35a01b6127 Merge pull request #660 from Xaekai/UstreamURLs
Fix UStream URL Generation
2017-05-14 18:38:33 -07:00
Xaekai
8dae6e66cc Add a simple listener for SIGUSR2 to reload SSL certs 2017-05-14 04:20:58 -07:00
Xaekai
c6065dbd95 Add tab completion entry for reloading cert 2017-05-14 04:10:18 -07:00
Xaekai
8b95b9fc41 Fix UStream URL Generation 2017-05-14 04:02:32 -07:00
Calvin Montgomery
de309d675e Remove redundant signing logic from IP session cookie 2017-05-01 21:51:11 -07:00
Calvin Montgomery
6bfbbc0c01 Support hot-swapping HTTPS certificates 2017-04-30 17:20:19 -07:00
Calvin Montgomery
e92afcb203 Fix error logger to only log if error is non-null 2017-04-30 16:49:23 -07:00
Calvin Montgomery
a0af0ccab5 Remove dead/commented-out code 2017-04-29 17:08:43 -07:00
Calvin Montgomery
089ac75e9a Fix DB purge of expired password reset reqs
3 year old bug introduced when refactoring 2.x -> 3.0.
Never worked in the first place.
2017-04-29 17:05:45 -07:00
Calvin Montgomery
8e74b0c765 Tweak setting description for playlist_max_ruation_per_user 2017-04-29 16:50:56 -07:00
Calvin Montgomery
fac94d46a6 Bugfix: stringify first parameter to Logger.xxx() 2017-04-27 21:06:16 -07:00
Calvin Montgomery
5b58c30011 Fix TypeError 2017-04-24 22:31:51 -07:00
Calvin Montgomery
ef9c744003 Use HTTPS in formatURL 2017-04-11 21:57:05 -07:00
Calvin Montgomery
8d40c87dda Deprecate jwplayer and googleplus videos 2017-04-11 21:55:31 -07:00
Calvin Montgomery
25c663c110 Fix node deprecation warning about calling fs.writeFile without cb 2017-04-04 23:12:02 -07:00
Calvin Montgomery
8306d2d1b6 Refactor logging 2017-04-04 23:02:31 -07:00
Calvin Montgomery
b1a328d2e0 Implement max total video time per user 2017-04-03 21:18:40 -07:00
Calvin Montgomery
f42e3bf2b7 Fix #656 2017-04-03 20:31:21 -07:00
Calvin Montgomery
5bdf8b4aaf Fix #657 2017-03-26 11:13:10 -07:00
Calvin Montgomery
0ce6fbba20 Fix an issue with playlist item matching 2017-03-26 11:04:02 -07:00
Calvin Montgomery
5e537fa8db Instead, add env var for overriding user input 2017-03-21 20:27:29 -07:00
Calvin Montgomery
61f872bb84 Add before_script to run build-server 2017-03-21 20:21:58 -07:00
Calvin Montgomery
cc23fd5273 Add .travis.yml 2017-03-21 20:08:10 -07:00
Calvin Montgomery
7595faf11d Fix voteskip issue when there are no videos left 2017-03-21 20:04:06 -07:00
Calvin Montgomery
309cd40da2 Compare owner name case-insensitively when deleting channel 2017-03-21 19:47:31 -07:00
Calvin Montgomery
0613083eb0 Handle the case where no socket.io ack exists 2017-03-20 22:09:16 -07:00
Calvin Montgomery
9dc82ad444 Enforce stricter validation on polls 2017-03-20 21:37:32 -07:00
Calvin Montgomery
41a538c655 Fix playlist visibility: wait for U_HAS_CHANNEL_RANK instead of just login 2017-03-18 18:53:49 -07:00
Calvin Montgomery
d8b9e3dab6 Merge pull request #648 from zeratul0/patch-1
Change fallback voteskip ratio to .5 from 0
2017-03-16 21:48:24 -07:00
zeratul0
88044e11d5 Error response -> template literal as requested 2017-03-16 23:34:38 -04:00
zeratul0
ab1358df36 Change skipratio fallback to error response 2017-03-16 18:35:42 -04:00
Calvin Montgomery
a594b19745 Fix user join ban check for users with blank names (but clean IPs) 2017-03-15 23:44:03 -07:00
Calvin Montgomery
f6500ff745 Fix emote regex due to sanitize-html changes 2017-03-14 21:36:44 -07:00
Calvin Montgomery
9239c2d465 Add channels.owner_last_seen column 2017-03-13 21:05:32 -07:00
Calvin Montgomery
8f266ccd44 Add channels.last_loaded column 2017-03-13 20:55:06 -07:00
Calvin Montgomery
d62d5e0cab Add NEWS entry about the name_dedupe column 2017-03-13 20:33:06 -07:00
Calvin Montgomery
c721d67080 Add explicit confirmation that accounts are unrecoverable with no email 2017-03-11 17:22:31 -08:00
Calvin Montgomery
f8183bea1b Add name_dedupe column instead of using LIKE kludge for similar-looking names 2017-03-11 17:09:50 -08:00
zeratul0
d93e42a71c Change fallback voteskip ratio to .5 from 0
moderators might make a mistake changing the skip ratio, causing it to fall back to 0% and allowing users to "skiptrain" before it is fixed
2017-03-06 13:44:05 -05:00
Calvin Montgomery
4701e767b6 Another fix for the new emote code 2017-03-03 23:59:07 -08:00
Calvin Montgomery
d65cf1beef Change sanitize-html back to the upstream module 2017-03-03 23:51:47 -08:00
Calvin Montgomery
a56f0d5b10 Adjust google drive userscript prompt 2017-03-03 23:39:38 -08:00
Calvin Montgomery
aea456436e Fix race condition for siteadmin rank socket frames 2017-03-03 23:34:27 -08:00
Calvin Montgomery
6672f5f75e Minor fix to new emote method 2017-03-02 18:53:34 -08:00
Calvin Montgomery
70be35e3fa Experimental ustream fix 2017-03-02 18:47:47 -08:00
Calvin Montgomery
20326194f7 Add execEmotesEfficient behind feature flag
For #645.  Disabled by default, I'll selectively enable it to be sure it
works and then remove the old implementation.
2017-03-01 21:16:55 -08:00
Calvin Montgomery
d4db459ff9 Fix #647 2017-03-01 20:46:01 -08:00
Calvin Montgomery
a80512aa60 Fix #646 2017-02-23 20:10:57 -08:00
Calvin Montgomery
5487d15bdf Add config option for mysql pool size, optimize restart login flood case 2017-02-02 23:05:50 -08:00
Calvin Montgomery
3020060627 Merge pull request #642 from h2v4c/voteskipfix
Update voteskip.js
2017-01-30 22:00:28 -08:00
h2v4c
022fda3d1c Update voteskip.js
fixed an issue where the voteskip socket emit would send {0 count, 0 need} upon voteskip passing
2017-01-25 12:54:04 -06:00
Calvin Montgomery
97de993055 Rename shit to avoid breaking backwards compat of currenttitle 2017-01-23 22:00:18 -08:00
Calvin Montgomery
d7c3edfac5 Fix for video resizing 2017-01-23 21:56:11 -08:00
Calvin Montgomery
2c57719318 Support changing the ratio of video:chat width 2017-01-23 21:47:21 -08:00
Calvin Montgomery
b0ff4d5ef0 Make delete from channel library a configurable permission 2017-01-23 21:16:39 -08:00
Calvin Montgomery
bec55bc3d1 Add message for /clear 2017-01-23 21:06:42 -08:00
Calvin Montgomery
27e168ba8b Integrate new tab completion methods
There is now an option to choose which tab completion method to use.
Also, emotes can be tab completed.
2017-01-10 22:26:46 -08:00
Calvin Montgomery
e1ad7c63af Clarify custom embed error to remove the clause about switching to plain HTTP 2017-01-09 23:47:11 -08:00
Calvin Montgomery
ee8cf35cdf Add checks for a couple JSPREF edge cases 2017-01-09 21:02:42 -08:00
Calvin Montgomery
5321996c64 Implement tab cycle style completion (not used anywhere yet) 2017-01-07 10:55:59 -08:00
Calvin Montgomery
dfdc07cbfa Start working on better tab completion
Code is not used anywhere yet, but the end goal is:
* Replace the bash-style algorithm with a less kludgy one
* Add the ability to customize tab completion method (will also
  include default zsh-style completion)
* Abstract tab completion so it can be shared for chat and emote names
  as available options
2017-01-06 20:10:33 -08:00
Calvin Montgomery
34ca5e12af 2016 -> 2017 2017-01-05 23:05:09 -08:00
Calvin Montgomery
6e61a13354 Add tests missed in last commit 2017-01-05 21:04:15 -08:00
Calvin Montgomery
e2abb90d14 Add HTTPS check for ffmpeg and custom embeds 2017-01-05 20:58:07 -08:00
Calvin Montgomery
5f4e9076df Fix ustream to be https 2017-01-05 20:31:20 -08:00
Calvin Montgomery
31880fa625 Fix an issue where one broken channel can prevent others from saving
Son of a bitch.
2016-12-28 23:24:08 -08:00
Calvin Montgomery
b0daa58874 Remove debug conditional 2016-12-20 00:10:17 -08:00
Calvin Montgomery
f6c201f3ba Add a few safeguards around channel saving 2016-12-20 00:09:24 -08:00
Calvin Montgomery
d21943ecc7 Whitelist m4a/aac for ffmpeg 2016-12-17 19:53:17 -08:00
Calvin Montgomery
041d50cb23 Merge pull request #636 from calzoneman/deprecate-http
HTTPS enforcement phase 1
2016-12-15 22:57:06 -08:00
Calvin Montgomery
8719527a31 Enforce HTTPS for new profile images 2016-12-13 22:44:23 -08:00
Calvin Montgomery
53d385f53e Copy CSS URL validation for JS 2016-12-13 22:22:25 -08:00
Calvin Montgomery
453ed607ba [http deprecation] enforce HTTPS for externalcss URLs 2016-12-10 23:23:57 -08:00
Calvin Montgomery
e8d39850c5 Fix null check for youtube livestream check 2016-11-30 09:24:28 -08:00
Calvin Montgomery
a624f45493 Fix warning in node 7 2016-11-17 23:01:20 -08:00
Calvin Montgomery
aa06884bd6 Ignore cached metadata for youtube livestreams 2016-11-17 23:00:06 -08:00
Calvin Montgomery
632ffdfa8f deps: upgrade yamljs
Old version of `yamljs` was bringing in deprecated dependencies causing
`npm install` warnings.  Newer version is still buggy and doesn't
fully support the YAML spec, but it seems to work at least as well as
the old version, as far as I can tell.
2016-11-02 22:55:14 -07:00
Calvin Montgomery
9302a271d0 Remove default contact config 2016-11-01 22:44:26 -07:00
Calvin Montgomery
bfad626b2d Merge pull request #632 from calzoneman/partition-refactor
Refactor partitioning a bit
2016-10-25 20:21:34 -07:00
Calvin Montgomery
afa18c4749 Fix Google Drive URL 2016-10-20 19:07:03 -07:00
Calvin Montgomery
d159a16aca Add configuration for redis key 2016-10-18 23:13:25 -07:00
Calvin Montgomery
654d57b53e Add CLI for loading/saving partition map 2016-10-16 16:58:28 -07:00
Calvin Montgomery
7117cd0a5e Fix typo 2016-10-15 16:09:27 -07:00
Calvin Montgomery
d2cce4f166 Work on auto reloading partition map from redis 2016-10-15 12:36:20 -07:00
Calvin Montgomery
3c11ac6cf5 Add jitter and retry logic to google drive userscript lookups 2016-10-08 10:33:18 -07:00
Calvin Montgomery
d0d2002a5f Fix some drive userscript issues 2016-10-07 19:55:41 -07:00
Calvin Montgomery
7c3f2d0a8b only set channel rank for non-guest 2016-10-06 23:22:02 -07:00
Calvin Montgomery
ad4ee4bd02 Fix profile/rank for bot logins 2016-10-06 23:01:42 -07:00
Calvin Montgomery
99760b6989 Purge the awful refreshAccount logic
User.prototype.refreshAccount was responsible for multiple race
condition bugs as well as inefficient duplication of DB queries in an
attempt to correct such race conditions.

It has now been replaced by a more reasonable model:

  * Global user account information and aliases are fetched in parallel
    on socket connection
  * Channel rank is fetched when the user tries to join a channel
2016-10-03 23:12:22 -07:00
Calvin Montgomery
014eb28e0d Fix regression for user profiles 2016-10-01 21:37:42 -07:00
Calvin Montgomery
35a8e2b52a Fix age old bug with /login redirecting to /register after registration 2016-10-01 21:31:04 -07:00
Calvin Montgomery
c88c63a422 Merge getGlobalRank and getProfile into one query
Really the entire "Account" thing needs to be refactored/deleted and
replaced with separate global account and per-channel state, which I
plan to do, but this brings some minor benefit in the meantime
2016-09-26 22:36:17 -07:00
Calvin Montgomery
e1120455b2 Cache channel ID for quicker loads/saves 2016-09-26 22:20:58 -07:00
Calvin Montgomery
b4b23f748f Change Hitbox player to HTTPS (#627) 2016-09-19 20:45:21 -07:00
Calvin Montgomery
489c0933e8 Upgrade to babel 6 2016-09-18 22:08:43 -07:00
Calvin Montgomery
83987afd73 Add config for twitch client ID 2016-09-18 21:35:08 -07:00
Calvin Montgomery
edff85dfb0 Fix User#inChannel for channels with passwords 2016-09-17 15:02:30 -07:00
Calvin Montgomery
1b1d2596f8 Change default for new account delays to 0 2016-09-14 22:06:00 -07:00
Calvin Montgomery
cbfbf396dd Merge pull request #623 from calzoneman/twitch-vod
Add twitch vod support
2016-09-10 12:04:06 -07:00
Calvin Montgomery
f62d9bc271 Add twitch vod support 2016-09-04 18:53:38 -07:00
Calvin Montgomery
7b4126c32f Add concurrency to channel saving on server shutdown 2016-08-31 21:46:54 -07:00
Calvin Montgomery
5b60a48c7f Fix double save when reloading partition map 2016-08-31 21:33:56 -07:00
Calvin Montgomery
8b94c54d25 Fix bug causing channels to get stuck when DB is down 2016-08-31 21:32:42 -07:00
Calvin Montgomery
ced2719f0e Document chat account age restrictions 2016-08-30 21:32:27 -07:00
Calvin Montgomery
84fa7972e3 Skip minification for userscript 2016-08-30 21:20:42 -07:00
Calvin Montgomery
d821fc6ccd Replace Twitch.TV player
Replaced the flash player with their HTML5 API.  This also lays the
groundwork for VOD playback.
2016-08-24 20:21:49 -07:00
Calvin Montgomery
e17dac58fd Add a temp bypass for initial rollout 2016-08-24 20:03:30 -07:00
Calvin Montgomery
b34a8fce3c Merge pull request #614 from calzoneman/ip-session-age
Restrict chat messages from newer accounts/IPs
2016-08-24 19:49:46 -07:00
Calvin Montgomery
377512340a Bump package version 2016-08-24 19:49:26 -07:00
Calvin Montgomery
af6e958c49 Merge remote-tracking branch 'origin' into ip-session-age 2016-08-24 19:48:58 -07:00
Calvin Montgomery
459ae4dec8 Merge pull request #618 from calzoneman/gdrive-userscript
Implement last resort solution: Google Drive userscript
2016-08-24 19:14:57 -07:00
Calvin Montgomery
5a81ab7ce7 Add a prompt explaining the situation as well as documentation 2016-08-23 21:50:18 -07:00
Calvin Montgomery
578d3fbb23 Add workaround for GM sandbox and refactor userscript a bit 2016-08-20 10:59:20 -07:00
Calvin Montgomery
8d3b2e59df Shut up tampermonkey about redefined variables 2016-08-15 21:16:14 -07:00
Calvin Montgomery
ba9fbea1a1 Minor fixes/cleanup 2016-08-15 21:09:43 -07:00
Calvin Montgomery
4feee02e33 Add initial userscript 2016-08-15 21:00:56 -07:00
Calvin Montgomery
d51722c466 Merge pull request #616 from calzoneman/gd-html5-hack
Gd html5 hack
2016-08-11 21:32:44 -07:00
calzoneman
6ebd4af490 Add video.js source link fallback 2016-08-11 21:07:06 -07:00
calzoneman
050dec4d0f Fix initialization race condition and make it toggleable 2016-08-11 20:25:06 -07:00
calzoneman
af663bfbcf Implement HTML5 hack for google drive
Requires enabling in config.yaml
google-drive:
  html5-hack-enabled: true

Requires a recent version of node and an IPv6 address

EXPERIMENTAL
2016-08-11 20:04:51 -07:00
calzoneman
d9d385f85e Fix an age old log message missing whitespace 2016-08-10 23:10:44 -07:00
calzoneman
17aad006f7 Fix startup issue 2016-08-10 22:37:33 -07:00
calzoneman
33f775051d Fixes for bot logins 2016-08-10 22:20:53 -07:00
calzoneman
05b40b8091 Bump version number 2016-08-10 22:10:17 -07:00
calzoneman
0327b3de2e Modifications for ip session cookie 2016-08-10 22:10:02 -07:00
calzoneman
74cb1b3efc Implement time parsing/formatting for channel settings 2016-08-10 21:59:36 -07:00
calzoneman
8305c235eb Add initial channel setting for new account chat delay 2016-08-10 21:59:30 -07:00
calzoneman
701d470494 Add initial blocking of new users in chat 2016-08-10 21:59:18 -07:00
Calvin Montgomery
f9ccb1509b Merge pull request #613 from calzoneman/videojs-hls
Add HLS support (and upgrade Video.JS)
2016-08-09 22:42:12 -07:00
calzoneman
e99bfcd47b Merge branch '3.0' into videojs-hls 2016-08-09 22:41:55 -07:00
calzoneman
6245dc84da Minor bug fix 2016-08-08 23:04:34 -07:00
calzoneman
016b125f49 Initial IP session cookie implementation 2016-08-08 23:03:16 -07:00
calzoneman
7b95777d99 Add a few things missed last commit 2016-08-08 20:34:03 -07:00
calzoneman
96a5d657a5 Merge branch 'streamable-1' into 3.0 2016-08-07 21:27:38 -07:00
calzoneman
ac94d6ba22 Merge branch '3.0' into streamable-1 2016-08-07 21:27:27 -07:00
calzoneman
d06c614ccc Add HLS support (and upgrade Video.JS) 2016-08-06 21:14:52 -07:00
calzoneman
da99ea8288 Add node version check to index.js 2016-08-04 19:00:36 -07:00
Calvin Montgomery
38c3883c01 Merge pull request #609 from jarrpa/jarrpa-patch-1
Fix URL in package.json
2016-08-04 17:28:32 -07:00
Jose A. Rivera
c0fc4c7a86 Fix URL in package.json
The dependency on status-message-polyfill appeared to be missing part of its URL, judging by the rest of the file. Changing this line allowed me to build and install on a self-hosted server.
2016-08-04 15:48:41 -05:00
calzoneman
8ebfb431ce Fix a bug with vid.me and streamable.com error handling 2016-08-02 22:40:29 -07:00
calzoneman
88c42af139 Add streamable.com support (#585) 2016-08-02 22:35:00 -07:00
Calvin Montgomery
6aebe82298 Merge pull request #604 from Xaekai/phoning.it.in
Prevent rendering of <iframes> and <objects> in the error message …
2016-07-25 20:24:13 -07:00
Xaekai
285dab9ed7 Prevent rendering of <iframes> and <objects> in the error message when attempting to queue them as supported host links instead of custom embeds.
I thought about just moving the parse failure message handling to queueMessage, but that quickly turned into a minefield of pain. This gets the job done for now.
2016-07-22 19:22:15 -07:00
Calvin Montgomery
71c5fe2a05 Merge pull request #603 from Xaekai/servicesocket
Add a service socket
2016-07-21 22:01:56 -07:00
Xaekai
9655d2635a Minor fixes 2016-07-21 18:17:38 -07:00
Xaekai
670cb97e79 Complete rewrite of the service socket client
Add one more command to the service commandline
2016-07-20 03:22:57 -07:00
calzoneman
6e416fea8a Add a hack to detect distrust of Let's Encrypt
Many older devices do not support the Let's Encrypt CA, for various
reasons.  This causes connection issues for sites using Let's Encrypt to
support HTTPS connections.  This commit adds a hack that can be enabled
with a switch in callbacks.js to try to detect when the user's browser
does not trust the certificate and permit the user to connect to an
insecure endpoint instead.

Unfortunately, the AJAX API does not allow to distinguish between *why*
a request fails, so the best we can do is detect that the HTTPS request
failed, try to make a request over plain HTTP, and if it works, assume
the HTTPS request failed due to a certificate error.  It's not 100%
foolproof since the HTTPS endpoint could just be down for some reason,
but it should work well enough in most cases.

Closes #602
2016-07-17 16:30:35 -07:00
Xaekai
9559035118 Add a service socket to enable out of band access to the process commandline 2016-07-16 11:05:32 -07:00
Calvin Montgomery
aaa21aad05 Merge pull request #598 from Xaekai/imagestrip
Image strip option
2016-07-14 23:44:49 -07:00
Xaekai
aded7b1f38 Allow users to strip images from chat. 2016-07-14 23:37:29 -07:00
Calvin Montgomery
31a392cea9 Merge pull request #599 from calzoneman/improve-cs-emotelist
Greatly improve performance of channel settings emote list
2016-07-14 23:26:30 -07:00
calzoneman
29a4834baa Add a signature to announcements
The `from` field has existed for ages, but was never actually displayed.
Displaying it to users reduces confusion about who is making the
announcement.
2016-07-14 23:25:17 -07:00
calzoneman
9e00bb133e Fix #601 2016-07-14 23:19:40 -07:00
calzoneman
d01d558ed6 Fill in incomplete CSEmoteList changes 2016-07-12 23:04:07 -07:00
calzoneman
ce260e0f5c Greatly improve performance of channel settings emote list
The channel settings emote list is now paginated and leverages the same
basic code as the emote browser, but with a different renderer.  Fixes
 #594 and kills an ugly function.
2016-07-11 23:55:07 -07:00
Calvin Montgomery
97cb751573 Merge pull request #597 from Xaekai/flairpersist
Make modflair setting persistent from button too.
2016-07-11 22:21:15 -07:00
Xaekai
b2b034d9df Make modflair setting persistent from button too. 2016-07-11 22:14:26 -07:00
Calvin Montgomery
5e399b96cf Merge pull request #596 from Xaekai/acpnav
Move ACP nav entry to templating
2016-07-11 22:11:59 -07:00
Xaekai
5eebd88e13 Move ACP nav entry to templating
Closes #516
2016-07-10 23:23:46 -07:00
Calvin Montgomery
44cc6336b9 Merge pull request #593 from Xaekai/themesanity
Default theme reference refactor
2016-07-10 20:46:33 -07:00
calzoneman
2a2ed7ce1c Fix partitioning for channels with capital letters 2016-07-09 01:51:06 -07:00
Xaekai
aa6066dfd5 Reduce the number of places the default theme is hardcoded from 6 to 1. 2016-07-08 19:01:31 -07:00
Calvin Montgomery
0de5f88eee Merge pull request #592 from Xaekai/fixcomma
Add missing comma
2016-07-08 00:10:49 -07:00
Xaekai
0bafe9f2d7 Add missing comma 2016-07-08 00:04:20 -07:00
Calvin Montgomery
2ae5af096b Merge pull request #591 from Xaekai/fuckitwelldoitlive
Add missing formatter entry for vid.me
2016-07-07 23:46:57 -07:00
Xaekai
0aa73a4b14 Add missing formatter entry for vid.me 2016-07-07 23:32:09 -07:00
Calvin Montgomery
292efd2b71 Merge pull request #589 from Xaekai/polltime
Add timestamps to polls
2016-07-07 23:30:19 -07:00
Calvin Montgomery
32bb63e06b Merge pull request #588 from Xaekai/moretags
Add some various harmless tags to the XSS whitelist
2016-07-07 23:06:46 -07:00
Xaekai
c3cd84f7af Merge remote-tracking branch 'upstream/3.0' into polltime 2016-07-07 23:06:12 -07:00
Xaekai
9cb2f2f0d3 Merge branch '3.0' into polltime 2016-07-07 22:57:46 -07:00
Xaekai
5896a1c0eb Add timestamps to polls.
Closes #562
2016-07-07 22:56:06 -07:00
Xaekai
42cf772dc3 Merge branch '3.0' into moretags 2016-07-07 22:51:08 -07:00
Calvin Montgomery
7025a70034 Merge pull request #587 from Xaekai/jade2pug
Update from Jade to Pug
2016-07-07 22:33:01 -07:00
Xaekai
9f4d2c7ffb Add some various harmless tags to the XSS whitelist
sub, sup: Closes #579
cite, small: Bootstrap uses these for blockquotes
template: Will allow for cleaner channel scripts. Since it's contents are inert it will also allow channel admins to have "comments" in their banner.
2016-07-07 21:52:34 -07:00
Xaekai
df5c5cd54f The Puggening: Update from Jade to Pug
1.) module dependency updated from jade 1.11.0 to pug 2.0.0-beta3
2.) All references to Jade have been changed to Pug
3.) /srv/web/jade.js is renamed to pug.js
4.) all template files renamed accordingly
5.) "mixin somename" is automatically considered a declaration, invocations must use "+somename"
6.) variable interpolation is no longer supported inside element attributes, use direct references and string concatenation instead.
7.) bumped minor version
2016-07-07 21:48:09 -07:00
calzoneman
f75d40d278 Fix a bug with passwords for bot logins 2016-07-03 23:50:16 -07:00
calzoneman
edb5fb6f4e Sync announcements across partitions 2016-07-03 21:28:43 -07:00
calzoneman
312892e56b Short term additional fix for #583
The previous commits do not handle all of the edge cases of #583
appropriately.  This is a short term solution that will work, but is not
as efficient as it could be.  The whole refreshAccount function needs to
be reconsidered and replaced with a more sane way of handling atomic
updates to the user's account state.
2016-06-29 22:00:25 -07:00
calzoneman
c70dc83504 Fix previous fix 2016-06-26 21:15:33 -07:00
calzoneman
e9fdb1a7e5 Fix login race condition (#583) 2016-06-26 16:21:15 -07:00
Calvin Montgomery
8ede986d22 Merge pull request #582 from calzoneman/vidme
Add vidme support (#568)
2016-06-26 16:20:42 -07:00
calzoneman
6f56862307 Add vidme support (#568) 2016-06-25 17:09:48 -07:00
calzoneman
056b2a48ea Add throttling of usercount frames 2016-06-18 00:32:50 -07:00
Calvin Montgomery
e4decbc34f Merge pull request #580 from calzoneman/partitioning
Implement sharding of channels across multiple instances
2016-06-18 00:13:28 -07:00
calzoneman
77d84d5b76 Add redis client error listener 2016-06-13 23:09:27 -07:00
calzoneman
b6bb0aa56d Add redis-based channel index 2016-06-09 23:42:30 -07:00
calzoneman
5b9948f709 Omit the connection warning if the socket connected at least once before 2016-06-08 22:58:34 -07:00
calzoneman
6e772c6837 Add partition map reload 2016-06-08 22:54:16 -07:00
calzoneman
7faf2829b2 Improve clientside socket.io connection error reporting 2016-06-07 23:00:50 -07:00
calzoneman
a360cd8808 Reject joins for channels mapped to other partitions 2016-06-07 22:47:49 -07:00
calzoneman
77465e6b49 Add partitioning logic 2016-06-06 21:54:49 -07:00
calzoneman
5f773d46c9 Fix poll timestamps with no unit 2016-06-05 23:01:03 -07:00
calzoneman
0a94da4d13 Use Promise.reduce() on shutdown to prevent overloading memory 2016-05-25 18:56:20 -07:00
calzoneman
594a9e17da Spread channel saves across the save interval
Since all channels were saved sequentially, this would cause huge lag
spikes every time the channel save interval fired.  This change adds a
delay between each channel so that the additional load is spread evenly
across the save interval.
2016-05-25 18:56:20 -07:00
calzoneman
5a2aa396fe Fix #575 2016-05-25 17:39:22 -07:00
calzoneman
aedd0df228 Limit the number of channels displayed on the index page 2016-05-21 16:59:28 -07:00
calzoneman
beb99c5632 Improve UI for new poll timeout entry
Resolves #565
2016-05-21 16:52:48 -07:00
calzoneman
ae3f4bbf0b Bump version number 2016-05-21 16:29:22 -07:00
calzoneman
f8a4652533 Fix #572 2016-05-21 16:26:32 -07:00
calzoneman
0922ce8e66 Remove dead client code 2016-05-21 16:24:41 -07:00
calzoneman
7e623daebb Fix #573 2016-05-21 16:18:52 -07:00
calzoneman
a00820a4c6 Fix queue progress bar for youtube playlists 2016-05-21 16:13:58 -07:00
calzoneman
75245e4d98 Include video ID in the progress bar to prevent false clears 2016-05-19 21:31:10 -07:00
calzoneman
8ed50d0b08 Add progress bar to the queue 2016-05-19 21:24:06 -07:00
calzoneman
d357b30f9d Add raw video documentation 2016-05-19 21:07:42 -07:00
calzoneman
b35b2a6e7e Clean up ffprobe error reporting
The error messages were a bit difficult for users to understand due to a
combination of including useless technical information and lacking
userful user information.  The error messages are more verbose now and
give a better indication of what went wrong.
2016-05-19 20:51:39 -07:00
calzoneman
fe37cb198e Add channelCount metrics 2016-05-19 20:09:35 -07:00
calzoneman
58a193b63b Add back socket.io:count metric that was accidentally removed 2016-05-06 20:09:00 -07:00
calzoneman
d61005e419 Add rel=noopener noreferrer to autogenerated links 2016-05-04 20:58:18 -07:00
calzoneman
59c7571ad5 Fix emote click-to-insert behavior 2016-05-04 20:52:55 -07:00
calzoneman
46eaa7e090 Update to handle cytube-common logger initialization 2016-04-27 22:33:53 -07:00
calzoneman
e7866cabc8 Update NEWS.md 2016-04-27 22:06:52 -07:00
calzoneman
3b4800d045 Add database queryTime metric 2016-04-27 21:55:25 -07:00
calzoneman
72bd3e4c98 Add localStorage flag for connecting to alt server list 2016-04-26 21:57:11 -07:00
calzoneman
a33f3d8bb0 Remove debug console.log 2016-04-23 19:54:32 -07:00
calzoneman
b69bd82a72 Add DualClusterClient for live testing phase of backend/frontend split 2016-04-23 19:53:18 -07:00
calzoneman
295c2a41a8 Add socket.leave() support for proxy backend 2016-04-03 11:49:58 -07:00
calzoneman
0ee7f05213 Make polls more efficient
Instead of emitting frames to each individual socket, group them into
socket.io rooms of people who can see hidden poll results and people who
can't, then just do 2 broadcasts.
2016-04-02 11:57:26 -07:00
calzoneman
20538e328f Replace legacy emitter with EventEmitter prototype 2016-04-02 11:23:34 -07:00
calzoneman
a45148863a Fix #548 2016-03-31 22:51:05 -07:00
calzoneman
d59daab2ae Make EmoteList self-contained instead of referencing globals 2016-03-29 23:31:02 -07:00
calzoneman
4e011c0d26 Change modflair label to "Name Color"
Also combined adminflair and modflair into one button (the button has 2
states if you're a moderator, or 3 states if you're a site
administrator).  Resolves #517
2016-03-29 22:30:16 -07:00
calzoneman
9debebd4b9 /clean: error when no argument given 2016-03-28 22:31:35 -07:00
calzoneman
52e444ab64 ffmpeg: follow relative redirects 2016-03-26 11:43:26 -07:00
calzoneman
efdab32aa8 soundcloud: fix volume preservation
Soundcloud requires rebinding the READY event when a new track is loaded
in order to set the volume.
2016-03-26 11:25:04 -07:00
calzoneman
319c52911a Resolve #553 2016-03-23 23:04:58 -07:00
calzoneman
c5c40a0386 Address #560 by adding a 'Volume' label 2016-03-23 22:45:54 -07:00
calzoneman
7448429341 Fix #566
Refactored the ffprobe stream-selection logic to handle rejected files
better:

  * Streams tagged as a non-default disposition are not considered
  * If a file has any video stream, the audio stream will be ignored

This should prevent videos from being misreported as invalid audio
codecs, etc.
2016-03-21 23:28:21 -07:00
calzoneman
a96b85fa5b proxyinterceptor: handle SocketDisconnectEvent 2016-03-07 20:25:32 -08:00
calzoneman
d913f02657 player: call videojs.destroy() when transitioning 2016-03-02 19:31:29 -08:00
calzoneman
e07cc0f5aa Remove unused rtmpEventHandler 2016-03-02 19:19:47 -08:00
calzoneman
e88031f4c5 Fix rtmp->rtmp transition 2016-03-02 19:18:33 -08:00
calzoneman
2bfb143c4c Update copyright year (finally lol) 2016-03-01 22:59:14 -08:00
Calvin Montgomery
1eb91e8c3c Merge pull request #563 from XCanG/patch-1
Fixed profile image
2016-02-29 17:24:13 -08:00
XCanG
e2e864e93b Fixed profile image
See why: http://hnng.moe/f/8Q4
Now profile-image never be stretched.
2016-02-28 18:38:41 +05:00
calzoneman
908377b20c Set allowfullscreen for iframe embeds 2016-02-21 21:04:35 -08:00
Calvin Montgomery
6192de4bcb Create ISSUE_TEMPLATE.md 2016-02-17 19:38:47 -08:00
calzoneman
76ef8d6906 Improve performance of mass connects by broadcasting usercount 2016-02-15 21:35:59 -08:00
calzoneman
98d3090c7d Move BackendModule import 2016-02-15 17:21:28 -08:00
calzoneman
39e3978161 Fix 2016-02-09 23:04:07 -08:00
calzoneman
cada5f0b0a Actually make the legacy mode default 2016-02-09 22:59:48 -08:00
calzoneman
5de6be0850 Merge branch 'mp-backend' into 3.0 2016-02-09 20:14:08 -08:00
calzoneman
e6234297a1 Merge 2016-02-09 20:13:58 -08:00
calzoneman
2eb17f4c32 Fix MIME mapping for ogg/vorbis -> audio/ogg 2016-02-09 19:44:07 -08:00
calzoneman
b3c85e8534 Limit requestPlaylist to once per 60 seconds
If clients call it quickly in succession with large playlists, it can
cause node to get stuck stringifying socket.io frames and cause an out
of memory crash.
2016-02-06 19:40:50 -08:00
calzoneman
50124c8a45 Refactor backend initialization 2016-02-04 21:43:20 -08:00
calzoneman
65d4ea9496 Fix #555 2016-01-31 11:17:19 -08:00
calzoneman
ba54848db5 mediarefresher: fix memory leak from dangling timers 2016-01-30 19:42:55 -08:00
calzoneman
86abebf9bf Add RedisClusterClient 2016-01-28 19:51:59 -08:00
calzoneman
f8470fc8f6 Use new proxy address formatter 2016-01-23 12:46:04 -08:00
calzoneman
dd73a8ee9a Automatically publish backend address to the pool 2016-01-20 23:11:55 -08:00
calzoneman
eba787942c package: bump source-map-support 2016-01-09 11:59:23 -08:00
calzoneman
d7da01a7d0 package: bump cytubefilters 2016-01-08 00:08:08 -08:00
calzoneman
be0759069e package: bump cytubefilters 2016-01-07 22:15:21 -08:00
calzoneman
f46891b6ed Defer to mediaquery for anonymous vimeo lookup 2016-01-07 17:38:05 -08:00
calzoneman
eeaffe1f61 Update socket.io to version 1.4.0 2016-01-06 21:42:48 -08:00
calzoneman
1ac69709ee Minor fix to refcounter logic 2016-01-04 20:35:02 -08:00
calzoneman
865a7453d9 Undo HD layout before applying synchtube, fluid (#549) 2016-01-03 22:53:29 -08:00
calzoneman
8bef7924b2 Minor fix 2016-01-01 18:28:53 -08:00
calzoneman
28807344bc Import logger 2016-01-01 18:26:43 -08:00
calzoneman
cdb20e8d40 Handle when a frontend disconnects 2016-01-01 18:25:12 -08:00
calzoneman
9a262da13d Set socketUser data from frontend 2015-12-30 21:57:46 -08:00
calzoneman
5b44117681 Use new protocol 2015-12-28 23:52:39 -08:00
calzoneman
9dd617d9fc Update to reflect change in endpoint key 2015-12-27 15:10:43 -08:00
calzoneman
b536c15758 Initial hacks to get the split to work 2015-12-26 15:07:03 -08:00
calzoneman
be4011cda1 Replace old ActiveLock system with a slightly better one
CyTube has been crashing recently due to things attempting to release
the reference after the channel was already closed (apparently the
uncaughtException handler isn't called for this?).  This newer
implementation keeps track of what is ref'ing and unref'ing it, so it
can log an error if it detects a discrepancy.

Also changed the server to not delete the refCounter field from the
channel when it's unloaded, so that should reduce the number of errors
stemming from it being null/undefined.
2015-12-25 17:07:25 -08:00
calzoneman
10d4ec8e60 Initial work for proxy connections 2015-12-24 16:24:07 -08:00
calzoneman
e88971a011 Shorten index length for channel library table (#543) 2015-12-21 17:38:46 -08:00
calzoneman
01004c6a3f Improve require failure message as suggested in #546 2015-12-21 17:34:22 -08:00
calzoneman
04ffda7a20 Fix race condition in Chrome (#547)
When the changeMedia frame loads a new Google Drive video, @yt is still
set from before and moreover @yt.ready is still true, so calling play()
can result in a TypeError if the new embed hasn't loaded yet (this
seemed to happen consistently in Chrome and I was unable to make it
happen in Firefox).
2015-12-21 17:23:48 -08:00
calzoneman
5a2ef2d24d Minor fixes for queueWarn 2015-12-20 22:35:24 -08:00
calzoneman
e9e3cbb575 Update migrator to allow blacklisting/whitelisting keys to backfill 2015-12-18 19:21:11 -08:00
calzoneman
b4e7ab2443 Don't save a channel if it hasn't loaded yet 2015-12-18 19:20:57 -08:00
calzoneman
176d4cb06f Show custom title UI for fi: override (#523) 2015-12-13 11:18:46 -08:00
calzoneman
6f654b16b8 Prevent crash due to activeLock being destroyed before callback 2015-12-13 00:22:18 -08:00
calzoneman
9a4237cd00 Exclude siteadmins from channel limit (resolves #508) 2015-12-12 17:03:42 -08:00
calzoneman
bfe76dae0e Check X-Forwarded-For on sockets (resolves #528) 2015-12-12 16:59:58 -08:00
calzoneman
432ee7bc30 Add warning for inline CSS too large (fixes #538) 2015-12-12 16:49:40 -08:00
calzoneman
80c35b4190 Remove unused code 2015-12-12 16:28:24 -08:00
calzoneman
27af66075e Centralize x-forwarded-proto handling; fixes #542 2015-12-12 16:26:14 -08:00
Calvin Montgomery
1f9e396e05 Merge pull request #541 from lolcow/patch-2
Fix uid variable name duplication/ambiguity
2015-12-11 20:45:47 -08:00
Calvin Montgomery
cf67f1148f Merge pull request #540 from lolcow/patch-1
Use absolute path for counters.log
2015-12-11 20:45:41 -08:00
Lolcow Admin
29c0df4fcc Fix uid variable name duplication/ambiguity
`uid` is used twice, where it should be `uid` and `gid`, resulting in an attempted execution of something like `id -g 1500` rather than `id -g syncgroup`. These variable names are already confusing due to the nature of the functions, so I made it clear they're strings rather than numeric IDs.
2015-12-11 00:20:40 -05:00
Lolcow Admin
baf302f12c Use absolute path for counters.log
Puts it in line with the other uses of `Logger.Logger`. Allows running outside of pwd.
2015-12-11 00:03:18 -05:00
calzoneman
11d4c4ca62 Reject blank emote names and images 2015-12-05 18:52:39 -08:00
calzoneman
889fb6595f Add buffer zone to consider scrolling 'caught up' 2015-12-05 18:05:23 -08:00
calzoneman
b0d5e92350 Fix autoscrolling changes 2015-12-05 17:57:33 -08:00
calzoneman
59468ec77c Add safeguard to prevent #539 2015-12-02 20:59:46 -08:00
Calvin Montgomery
54cabc04e1 Merge pull request #537 from calzoneman/chatscroll-improvements
Chatscroll improvements
2015-11-30 17:18:35 -08:00
calzoneman
3c5d36919b Fix positioning and background color of new message indicator 2015-11-29 10:29:56 -08:00
calzoneman
23f39ab2f5 Improve chat autoscroll behavior
The previous behavior (don't autoscroll if the mouse is over the chat
area) was not intuitive and caused problems for people in chat only
mode, which led to a lot of people assuming that it was a glitch.

This change introduces the following behavior:

  * Hovering over chat no longer affects autoscroll.
  * Scrolling up in chat turns off autoscroll.
  * Scrolling to the bottom of the chatbox resumes autoscroll.
  * If a new message is added while autoscroll is off, a "New Messages
    Below" indicator is added to the bottom of the chatbox.
2015-11-29 09:49:21 -08:00
Calvin Montgomery
b241a210f3 Fix for uniqueness of IP range bans 2015-11-23 18:22:51 -08:00
calzoneman
5c50e93458 Use VideoJS for RTMP (#532) 2015-11-08 19:51:17 -08:00
calzoneman
f9e1d329e4 Remove reference to IO_URL in error handler
Referenced in #527
2015-11-06 20:03:01 -08:00
calzoneman
fac11ee312 Fix variable misuse 2015-11-03 19:34:12 -08:00
calzoneman
04336c9712 Fix merge error 2015-11-02 21:13:02 -08:00
calzoneman
47ef670f34 Fix typo 2015-11-02 21:10:52 -08:00
calzoneman
de9b963d38 Merge branch 'web-refactoring' into 3.0 2015-11-02 21:08:14 -08:00
calzoneman
5ead24e45e Merge branch '3.0' into web-refactoring 2015-11-02 21:07:50 -08:00
calzoneman
5c339656b7 Minor fixes 2015-11-02 20:52:57 -08:00
calzoneman
6505aa2f5e More refactoring 2015-11-01 17:42:20 -08:00
calzoneman
23333ee266 Remove console logging of clientErrors from HTTP/socket.io 2015-10-30 22:26:20 -07:00
calzoneman
edcf17984f Fix socket.io counters 2015-10-30 22:25:00 -07:00
calzoneman
44745d86ac Fix for Wii U browser
Apparently it doesn't send the login cookie if you explicitly set the
socket.io transports to prefer websockets.  Magic.
2015-10-29 20:50:10 -07:00
calzoneman
a8cc8e4b04 Add more counters for diagnostic information 2015-10-28 23:38:17 -07:00
calzoneman
c2726898e5 Move x-forwarded-for middleware 2015-10-27 23:54:32 -07:00
calzoneman
13d4a49976 Move contact page to its own route handler 2015-10-27 22:04:21 -07:00
calzoneman
88236e036c Add better error pages 2015-10-27 20:44:40 -07:00
calzoneman
26e8660af4 Change /logout from GET to POST (#515) 2015-10-26 23:21:09 -07:00
calzoneman
50ca141f1d Web refactoring 2015-10-26 22:56:53 -07:00
calzoneman
566e932e7e Reset LASTCHAT when chat is cleared 2015-10-25 17:31:04 -07:00
Calvin Montgomery
535b1d5d3a Merge pull request #522 from calzoneman/sioconfig-migration
Migrate socket.io configuration to new API
2015-10-25 17:25:00 -07:00
Calvin Montgomery
428007c3f8 Merge pull request #521 from OurFlagIsMined/patch-1
more than one Play click deleted a playlist item
2015-10-25 17:24:33 -07:00
calzoneman
1bdea33817 Correct typo 2015-10-25 17:22:50 -07:00
calzoneman
f5de173feb Remove extra word from NEWS.md 2015-10-25 17:21:15 -07:00
calzoneman
21c3a1b3cd API changes, add documentation 2015-10-25 17:20:39 -07:00
OurFlagIsMined
4809a3db00 more than one Play click deleted a playlist item
If two people tried to play the same playlist item, before the playlist updated, it would delete instead of playing.
The same would also happen if the play button was double-clicked instead of single-clicked.
Also, the active item's play button functioned as a delete button.

Fully tested. Still removes the item (if it was added as temporary) when it finishes playing, or if the play button of a *different* item is clicked.
2015-10-25 19:52:34 -04:00
calzoneman
b2a9c9c7a8 docs: add chat-filters.md 2015-10-21 23:04:49 -07:00
calzoneman
7b5476874d Minor function change 2015-10-21 20:56:09 -07:00
calzoneman
40e2a608f6 Initial sioconfig migration work 2015-10-19 22:32:00 -07:00
calzoneman
dacda65961 Prevent unnecessary duplicate profile boxes 2015-10-19 20:28:33 -07:00
calzoneman
6ed7ca8dbb Upgrade to video.js 5.0 2015-10-17 18:45:13 -07:00
calzoneman
e20537e0a5 Remove some old/unused files 2015-10-17 18:36:24 -07:00
calzoneman
8d39daf942 Factor out resumeAutolead() 2015-10-16 20:32:25 -07:00
calzoneman
217ed115a3 Fix #513 2015-10-16 20:23:41 -07:00
calzoneman
327b9faedb Add dbstore dumper script 2015-10-12 23:31:36 -07:00
calzoneman
b4b442c897 bgtask: run channel saves serially to prevent thrashing 2015-10-09 23:16:21 -07:00
calzoneman
7d35df4f5a Fixes 2015-10-07 22:19:39 -07:00
calzoneman
7f62e14045 index: add explicit error message for missing lib/server.js 2015-10-06 21:25:27 -07:00
Calvin Montgomery
9f4461a779 Merge pull request #507 from calzoneman/channel-store
Refactor channel storage to allow database store
2015-10-04 23:22:05 -07:00
calzoneman
2fe646ec03 Minor cleanup 2015-10-04 23:21:53 -07:00
calzoneman
1d33c47bfe package: bump version and update NEWS.md 2015-10-04 23:11:41 -07:00
calzoneman
bed7e65fc0 channel_data value should be a mediumtext 2015-10-01 22:13:16 -07:00
calzoneman
56a2a52bdd Fixes 2015-10-01 22:02:59 -07:00
calzoneman
0e66875d27 Add workaround for migrator error 2015-09-30 22:12:43 -07:00
calzoneman
81cbfc0639 Merge branch '3.0' into channel-store 2015-09-30 21:58:03 -07:00
calzoneman
9c5ada6134 Add config key for selecting storage mode 2015-09-30 21:55:45 -07:00
calzoneman
27b501e655 Add /sioconfig.json 2015-09-30 18:36:50 -07:00
calzoneman
7875dbdf4a Handle 'best' quality preference in VideoJSPlayer 2015-09-30 18:26:23 -07:00
calzoneman
a16f885fbd Fix custom embed invalid tag message 2015-09-28 17:31:37 -07:00
calzoneman
e91635b6f9 Implement migrator 2015-09-27 11:07:57 -07:00
calzoneman
22a4115217 webserver: update matcher for HTTP 416 errors 2015-09-27 09:33:42 -07:00
calzoneman
b2a4afd9ff Merge www/js/player.js from gdrive-youtube 2015-09-26 20:00:07 -07:00
calzoneman
c5e73e156a channel/chat: don't allow users to unmute themselves 2015-09-26 19:54:20 -07:00
calzoneman
10dbbcd3ff Fixes; initial migrator work 2015-09-26 15:33:13 -07:00
calzoneman
4bdd7a1e3b Add DatabaseStore 2015-09-26 14:21:42 -07:00
calzoneman
1ad41d7e58 Remove debug override 2015-09-24 23:50:50 -07:00
calzoneman
20dc871303 Use create-error for better error creation 2015-09-24 23:36:05 -07:00
Calvin Montgomery
237a4ae0e0 Merge pull request #506 from calzoneman/gdrive-youtube-hack
Gdrive youtube hack
2015-09-23 21:59:24 -07:00
calzoneman
5ec9c2b029 Start refactoring channel storage 2015-09-23 21:56:04 -07:00
Calvin Montgomery
f95f3dc89b Merge pull request #503 from calzoneman/es6
Transpile with babel for ES2015 support
2015-09-23 19:27:52 -07:00
calzoneman
2b6e58ace9 Add safeguard postinstall script prompt 2015-09-23 19:27:04 -07:00
calzoneman
0109a87e55 package: build with babel for ES2015 support
* Rename lib/ -> src/
* Add `postinstall` npm target for compiling src files to lib
* Add `build-watch` npm target for development with babel --watch
* Add `lib/` to .gitignore
* Add `source-map-support` module for babel-generated sourcemaps
2015-09-23 19:27:04 -07:00
Calvin Montgomery
d042619b21 Merge pull request #504 from calzoneman/deps-upgrade
Update dependencies
2015-09-23 19:18:45 -07:00
calzoneman
14bfaae487 Update dependencies 2015-09-23 19:15:28 -07:00
calzoneman
e85fd4bcd0 Add counters for stat recording 2015-09-17 23:29:32 -07:00
calzoneman
3356a55fbd Fix modal issue 2015-09-16 20:20:51 -07:00
calzoneman
c81ae989fd Add auth_timeout parameter 2015-09-16 00:04:34 -07:00
calzoneman
bfe36e8150 Hack YouTube flash player for Google Drive 2015-09-15 23:32:01 -07:00
Calvin Montgomery
f1c61bddb2 docs: update user-settings 2015-09-12 01:15:13 -07:00
calzoneman
5367d4e769 Add start of new user guide docs 2015-09-12 01:10:32 -07:00
calzoneman
c97332947d mediarefresher: fix google drive ignores 2015-09-03 18:17:08 -07:00
calzoneman
00fd0482a5 ffmpeg: fix issue where valid streams are overridden by invalid ones 2015-09-03 18:11:25 -07:00
calzoneman
bb3b9004e0 database: exit if initial connection fails (#500) 2015-09-02 23:03:47 -07:00
calzoneman
f2000b4459 Fix 2015-09-02 23:00:45 -07:00
calzoneman
5c35f0f39b setuid: fix folder permissions before dropping 2015-09-02 23:00:31 -07:00
calzoneman
de02cdbeff Intelligently split link lists to allow URLs with commas 2015-08-19 23:27:05 -07:00
calzoneman
26eb3502be Respect x-forwarded-proto for ACP 2015-08-17 20:20:58 -07:00
Calvin Montgomery
1e312ded4c Update README.md 2015-08-16 13:31:34 -07:00
calzoneman
91d81c4367 Respect X-Forwarded-Proto if it is set 2015-08-12 20:00:52 -07:00
calzoneman
a405c2c5fa Don't kill ffmpeg for HTTP 416 2015-08-11 18:25:14 -07:00
calzoneman
3cac6d2d10 Fix ffmpeg fd leak 2015-08-10 17:55:23 -07:00
calzoneman
6f7b34f644 Minor fixes to source quality sorting 2015-08-03 19:02:56 -07:00
calzoneman
b5efa18984 package: bump cytubefilters 2015-08-03 18:30:22 -07:00
calzoneman
9a5c71fddc Minor fix 2015-07-30 22:12:52 -07:00
calzoneman
b0f65ded80 Add TLS failure check 2015-07-30 21:11:30 -07:00
calzoneman
92b0747c0b Kill ffprobe after 30 seconds; add logging 2015-07-30 20:45:47 -07:00
calzoneman
66b08f53d1 Fix subtitles race condition 2015-07-28 20:34:55 -07:00
Calvin Montgomery
eb02ad0836 Merge pull request #499 from calzoneman/gdrive-captions
Support captions/subtitles for Google Drive videos
2015-07-27 17:42:32 -07:00
calzoneman
d86c62664c Fixes for Chrome 2015-07-26 13:29:06 -07:00
calzoneman
f12397db23 Minor fixes for Google Drive subtitles 2015-07-26 12:28:43 -07:00
calzoneman
523ebf4aea Fix video not being restored when getplaylist modal is closed 2015-07-26 11:41:54 -07:00
calzoneman
4a0cbce575 Use lang_original if subtitle name is empty 2015-07-25 11:46:18 -07:00
calzoneman
174ad8d81e Fixes for google drive subtitles 2015-07-25 10:31:21 -07:00
calzoneman
b0e6de389e Add gdrive subtitle docs 2015-07-25 01:27:41 -07:00
calzoneman
33e7f81fa7 Wire up google drive subtitles 2015-07-25 01:19:32 -07:00
calzoneman
6d9fc73701 Work on fetching/converting google drive subtitles 2015-07-22 21:24:37 -07:00
calzoneman
74e7b25877 Fix: playlist showing for guest when perm set to guest 2015-07-22 18:08:05 -07:00
Calvin Montgomery
dda5077727 Merge pull request #498 from calzoneman/comma-multi-queue
Add comma-separated queueing for multiple items
2015-07-21 21:46:28 -07:00
calzoneman
817e7ceb84 Fix underscore escaping for similar chars check 2015-07-20 19:07:58 -07:00
calzoneman
06347d6c25 Fix hidePlayer() 2015-07-16 21:43:21 -07:00
calzoneman
0044057287 Fix single-item playlist rendering glitch on Firefox 2015-07-16 21:28:11 -07:00
calzoneman
d76af73286 Add comma-separated queueing for multiple items 2015-07-16 19:14:55 -07:00
calzoneman
45c4e650ee Fix 2015-07-12 22:25:55 -07:00
Erik Little
b8794e2cf3 Merge pull request #496 from damianb/patch-1
Add license to package.json
2015-07-12 11:57:57 -04:00
calzoneman
7e75611e7f Fix fullscreen CSS for webkit browsers 2015-07-11 22:10:59 -07:00
Damian Bushong
397cc97e1e Add license to package.json 2015-07-11 22:13:08 -05:00
Calvin Montgomery
4100dfbc04 Sanity check for mediarefresher error 2015-07-09 21:47:11 -07:00
Calvin Montgomery
6e7225aa0d Change mediaurl keydown to keyup so that the Title box shows correctly 2015-07-09 21:45:41 -07:00
Calvin Montgomery
b2687f49a8 Update error messages caught by mediarefresher 2015-07-08 13:27:43 -07:00
calzoneman
cb52148b2f Replace lib/status-messages with status-message-polyfill 2015-07-08 11:00:40 -07:00
calzoneman
bba6b29483 Fix ustream embeds 2015-07-07 19:30:39 -07:00
Calvin Montgomery
d1f21573e6 customembed: don't parse in xmlMode
This fixes an issue where <params> weren't being parsed correctly
because they are self-closing HTML5 tags.
2015-07-07 12:28:09 -07:00
Calvin Montgomery
c212d32a6e Merge pull request #494 from calzoneman/player-fullscreen
Add fullscreen button
2015-07-06 22:38:50 -07:00
Calvin Montgomery
c0f76bcf00 Add fullscreen button 2015-07-06 17:35:04 -07:00
calzoneman
a6ddebbec3 Fix custom embeds in user_playlists; add NEWS.md
Server administrators should check NEWS.md before updating for
information about important changes or required administrator
intervention.
2015-07-06 12:49:21 -07:00
calzoneman
83e3c44a6d Merge branch 'player-rewrite' into 3.0 2015-07-06 11:28:30 -07:00
calzoneman
70be8a6713 Resolve merge conflict 2015-07-06 11:28:18 -07:00
calzoneman
2b6d980aeb Remove redundancy 2015-07-06 11:23:58 -07:00
calzoneman
e40db5f27e Fix mediaquery for contentType 2015-07-06 11:21:52 -07:00
calzoneman
87c1112508 Bump version number 2015-07-06 11:16:02 -07:00
calzoneman
e3d12007b3 Fix css/js textboxes on channelCSSJS frame 2015-07-05 17:52:51 -07:00
Calvin Montgomery
44f1091952 Remove redundancy by extending EmbedPlayer for Twitch 2015-07-05 14:04:21 -07:00
Calvin Montgomery
aad8eef52b Add LIVESTREAM_CHROMELESS flag 2015-07-05 14:00:59 -07:00
Calvin Montgomery
9f030376b5 Fix VideoJS volume 2015-07-05 13:50:34 -07:00
Calvin Montgomery
62ed922c73 Fix leader 2015-07-05 13:29:06 -07:00
calzoneman
35500822d2 Fixes 2015-07-03 11:24:21 -07:00
calzoneman
9db9856a4e Fix stuck loading spinner on IE 2015-07-01 23:59:21 -07:00
calzoneman
c422fa65fc Add fileplayer 2015-07-01 09:38:01 -07:00
calzoneman
a457ea6c8a Convert custom embeds when loading playlist 2015-06-30 09:38:19 -07:00
calzoneman
b34ea01c3d Changes to how custom embeds work 2015-06-29 18:32:18 -07:00
calzoneman
92d5375950 database: update for new custom embed format 2015-06-29 17:43:57 -07:00
calzoneman
7fee1414e2 Use VideoJSPlayer for vimeo workaround 2015-06-28 09:42:21 -07:00
Calvin Montgomery
c2b4033903 package: bump cytubefilters 2015-06-24 17:32:32 -04:00
calzoneman
b34f972629 Add imgur and ustream 2015-06-21 10:39:16 -04:00
calzoneman
8dd3280305 Add hitbox and update mixed content errors 2015-06-21 10:27:51 -04:00
calzoneman
b279a41122 Add RTMP player 2015-06-19 22:12:48 -04:00
Calvin Montgomery
4433260041 Merge pull request #490 from calzoneman/fix-489
Fix #489
2015-06-19 17:09:54 -04:00
calzoneman
c28dc0d3d2 Fix #489
Channels are occasionally plagued by trolls who confuse users by
"hijacking" names of other users in the channel.  This is accomplished
by replacing certain letters with visually similar letters (in fact,
indistinguishable in some sans-serif fonts), e.g. replacing lowercase
'l' with capital 'I'

This commit replaces capital 'I', lowercase 'l', digit '1', lowercase
'o', uppercase 'O', and digit '0' with '_' and changes the matching for
isUsernameTaken() to a LIKE query.  Since '_' is a single character
wildcard, this causes the database to treat a username with one of these
simple replacements as already registered.
2015-06-19 16:44:25 -04:00
calzoneman
f43e46c716 Fix loading no_emotes setting 2015-06-19 14:49:49 -04:00
calzoneman
14c25ef8c1 Add explicit warning that custom embeds cannot be synchronized 2015-06-19 08:39:11 -04:00
calzoneman
01fbd3c54e Work on custom embeds 2015-06-18 18:46:33 -04:00
calzoneman
60743bd2ea Add twitch player 2015-06-16 16:44:14 -04:00
calzoneman
cf5756227d Add livestream.com player 2015-06-16 12:50:17 -04:00
calzoneman
480497bea4 Initial soundcloud implementation 2015-06-16 07:39:39 -04:00
calzoneman
9451e3978c Cut down on unneccessary ffprobe error logging 2015-06-15 08:32:11 -04:00
calzoneman
32d9285560 Temporary fix for vimeoWorkaround 2015-06-14 06:41:01 -04:00
Erik Little
3f79e9f858 Merge pull request #487 from calzoneman/remove-yt2
Remove YouTube v2 API fallback since v2 is dead
2015-06-07 13:11:09 -04:00
calzoneman
0d8a389e05 Remove YouTube v2 API fallback since v2 is dead 2015-06-07 11:45:23 -04:00
calzoneman
d9f06a50de Fix 484 2015-06-04 00:57:51 -04:00
Calvin Montgomery
a4cd0659b6 Merge pull request #483 from calzoneman/ffmpeg-preflight
Preflight ffprobe requests with node to check headers/error conditions
2015-05-25 16:08:43 -04:00
calzoneman
a81f691d4e Allow 2 redirects 2015-05-25 16:04:27 -04:00
calzoneman
9b3a71d84f Fix typo 2015-05-24 11:23:43 -04:00
calzoneman
241db797d3 Add node comment to status-messages.js 2015-05-24 11:22:08 -04:00
calzoneman
18199b32ad Add status message map for pre-node v0.12 servers 2015-05-24 11:20:09 -04:00
calzoneman
334c0d933b Fix typo 2015-05-24 11:09:56 -04:00
calzoneman
c4add8f142 Preflight raw file requests to get better error messages 2015-05-24 11:06:02 -04:00
calzoneman
cd0cc69fd8 Gracefully handle HTTP errors in ffprobe 2015-05-23 14:36:13 -04:00
calzoneman
f94c8bc8f1 Replace &nbsp; with CSS margin 2015-05-22 10:29:24 -04:00
calzoneman
9081a5fee1 package.json: bump cytubefilters 2015-05-21 13:13:27 -04:00
Calvin Montgomery
854ff04c11 Merge pull request #480 from flussence/fix-1
Fix a typo
2015-05-21 13:02:56 -04:00
Anthony Parsons
35b2920c52
Fix a typo 2015-05-21 17:48:00 +01:00
Calvin Montgomery
0a0b125c8b Merge pull request #479 from calzoneman/ffmpeg-fixes
Resolve #476 - ffmpeg improvements
2015-05-20 20:45:53 -04:00
calzoneman
54f2ad7c5c Fixes 2015-05-19 22:07:55 -04:00
calzoneman
5f1f985dd0 Rewrite ffmpeg module 2015-05-19 19:48:08 -04:00
Calvin Montgomery
a38e4ef409 Merge pull request #478 from Xaekai/shadowmute
Fix shadowmuted users usage of greentext and slash commands
2015-05-19 13:02:17 -04:00
Xaekai
7f269784b1 Fix shadowmuted users usage of greentext and slash commands 2015-05-19 00:03:02 -07:00
calzoneman
88be0e1e92 Don't crash if ffprobe is missing 2015-05-16 23:36:04 -05:00
calzoneman
ce8ac4591e Use VideoJS for google+ 2015-05-15 01:19:08 -05:00
calzoneman
fe9ebfa6b1 Start working on VideoJS for Google Drive 2015-05-15 00:03:05 -05:00
calzoneman
7bc247ede2 Fix 'remove video' option 2015-05-14 13:14:45 -05:00
calzoneman
8b69485448 Show emote list button in chat only 2015-05-14 11:42:26 -05:00
calzoneman
c64d446b38 Fix button spacing when HTML is minified 2015-05-14 11:37:02 -05:00
Calvin Montgomery
d4e75d8ce8 Merge pull request #475 from calzoneman/emote-list
Emote list
2015-05-14 11:28:22 -05:00
calzoneman
2c90d28919 Tweak checkbox placement 2015-05-14 11:25:31 -05:00
calzoneman
86bd20d5cc Minor fix for emote insertion 2015-05-13 12:19:03 -05:00
calzoneman
8927613da7 Add emote search, sort toggle 2015-05-13 12:17:32 -05:00
calzoneman
d3e2433ee6 Fix emote background on light themes 2015-05-12 18:24:03 -05:00
calzoneman
691ec3055c Fix emote insertion behavior 2015-05-12 13:53:19 -05:00
calzoneman
389dd0d5ab Initial emote list implementation 2015-05-12 13:50:59 -05:00
calzoneman
73fc5dd724 Fix ffprobe title detection 2015-05-10 23:02:24 -05:00
Calvin Montgomery
e51793d60c Merge pull request #470 from Xaekai/3.0
Fix annoyance: Clicking Remove video scrolls to top
2015-05-07 17:07:18 -05:00
Xaekai
2dd1db166a Fix annoyance: Clicking Remove video scrolls to top 2015-05-06 12:14:56 -07:00
calzoneman
daf2463a6a Start working on Dailymotion 2015-05-05 15:06:37 -05:00
calzoneman
a291836a99 Change USEROPTS.default_quality values 2015-05-02 17:55:00 -05:00
calzoneman
d7b69bce38 Fixes 2015-05-02 17:37:09 -05:00
calzoneman
391ea264f5 Work on player rewrites 2015-05-02 11:45:35 -05:00
calzoneman
fa3ddbe94a Continue working on YouTube player 2015-04-30 15:26:52 -05:00
calzoneman
ae899fd9be Continue working on YouTube player 2015-04-30 15:26:09 -05:00
Calvin Montgomery
7902f1c3c6 Merge pull request #468 from calzoneman/chat-antiflood-limit
Update chat antiflood limit
2015-04-28 13:05:33 -05:00
Calvin Montgomery
1ff9f5648b Update MIN_ANTIFLOOD to be the same as the limits for channel settings 2015-04-27 13:09:32 -05:00
Calvin Montgomery
7782ba4ae5 Subject moderators to MIN_ANTIFLOOD rather than channel limit 2015-04-27 12:22:52 -05:00
Calvin Montgomery
7debb7afa7 Update limits for chat_antiflood_params 2015-04-27 12:21:21 -05:00
calzoneman
d77497aaa7 Work on YouTube player 2015-04-23 22:24:43 -05:00
calzoneman
e2c3b2daad Fix PM maxlength and throttling 2015-04-23 21:49:15 -05:00
calzoneman
f0c75211fb Start working on player rewrite 2015-04-23 21:40:08 -05:00
calzoneman
dd48da19c0 Add build-player script 2015-04-23 21:19:55 -05:00
Erik Little
2aba640ec5 Merge pull request #467 from calzoneman/confirmdelete
make sure the user understand this is for real
2015-04-20 16:26:51 -04:00
Erik
566c4c174e make sure the user understand this is for real 2015-04-20 16:09:21 -04:00
calzoneman
b8d2d74e2f Resolve #447 2015-04-19 14:14:38 -05:00
Calvin Montgomery
193385c88c Merge pull request #466 from Poniverse/3.0
Sanitized output of channel name in invalid channel
2015-04-18 22:57:53 -05:00
Adam Lavin
36290dfd5e Sanitized output of channel name in invalid channel 2015-04-19 04:38:57 +01:00
Calvin Montgomery
4a7e478f37 Merge pull request #465 from Xaekai/3.0
Allow mumble:// links through XSS filter
2015-04-16 10:45:07 -05:00
Xaekai
09de27644d Allow mumble:// links through XSS filter 2015-04-15 23:07:12 -07:00
Calvin Montgomery
84579c0d60 Add missing disconnect() in throttle 2015-04-10 12:05:27 -05:00
calzoneman
976228683b Add note to config template, fix improper err callback 2015-04-07 15:42:24 -05:00
calzoneman
5522628363 Fix Google+ 2015-03-31 15:58:46 -05:00
Calvin Montgomery
4d72ccb8a3 Merge pull request #461 from calzoneman/ytv3
Add YouTube v3 API
2015-03-31 12:01:36 -05:00
calzoneman
813ae3a2ef Minor updates to SSL behavior 2015-03-29 11:34:27 -05:00
calzoneman
f4a9f0b21b linewrap queue alerts 2015-03-27 19:08:58 -05:00
calzoneman
27a50cb702 Add YouTube v3 API
YouTube v2 is still supported as a fallback, but will log a warning
message to the error log as v2 is expected to be closed shortly after
April 20, 2015.

See also:
http://youtube-eng.blogspot.com/2015/03/dude-are-you-still-on-youtube-api-v2.html
2015-03-27 18:44:46 -05:00
Calvin Montgomery
9541b40f68 Merge pull request #460 from calzoneman/filter-changes
Change link avoidance mechanism in filters
2015-03-27 08:33:41 -07:00
Calvin Montgomery
7539dc1544 Merge pull request #459 from Xaekai/3.0
Fix rare error where only unsupported qualities of video are present in ...
2015-03-25 20:19:56 -07:00
Xaekai
4b65b9128a Fix rare error where only unsupported qualities of video are present in metadata on Picasa 2015-03-25 11:32:14 -07:00
Calvin Montgomery
0b86eeea6d Change link avoidance mechanism in filters 2015-03-24 09:23:38 -07:00
Calvin Montgomery
e1c8d5c6c9 Fix Ustream 2015-03-23 16:10:00 -07:00
calzoneman
3290501e81 Fix google drive/google+ 'highest available' quality 2015-03-20 14:23:44 -05:00
calzoneman
f3fe933f6e Support new google drive link format 2015-03-20 13:57:13 -05:00
Calvin Montgomery
69c7f38e1f Merge pull request #457 from 6IRCNet/3.0
set SSL ciphers
2015-03-06 22:06:17 -06:00
bush
fdf73b5908 I gotta stop using the tab key 2015-03-06 22:29:21 +00:00
bush
fb2d568c76 Merge remote-tracking branch 'upstream/3.0' into 3.0 2015-03-06 22:00:40 +00:00
bush
6138d3b7ff Fixes for calzoneman <3 2015-03-06 21:59:34 +00:00
calzoneman
e6bdbb99b1 Prefer MP4 over WebM because IE can't play webm apparently 2015-03-06 00:23:06 -06:00
calzoneman
950853ba71 cytubefilters: bump git hash 2015-03-05 14:31:58 -06:00
bush
f191e2a9c9 Fixed cipher list to high 2015-03-05 04:42:01 +00:00
bush
a9a77147be Enabled disabling of openssl cipher suits. RC4 disabled by default. 2015-03-05 04:31:45 +00:00
calzoneman
9b5d38e6c8 package.json: bump socket.io 2015-03-03 13:07:12 -06:00
Calvin Montgomery
8a669db1e3 Merge pull request #455 from gv1222/PR
Update modern theme
2015-03-02 16:51:40 -06:00
Graham
d2f4ad7e90 Improve connect and disconnect message css 2015-03-02 17:50:57 -05:00
Graham
982af6715d Fix alignment of controlsrow (again) 2015-02-28 14:12:14 -05:00
Graham
9b34035857 Fix 2px gap on chatbox 2015-02-28 14:12:09 -05:00
Graham
833cb1a284 Fix script access and other transparency issues 2015-02-28 13:24:59 -05:00
Calvin Montgomery
b64a97aa49 Merge pull request #456 from 6IRCNet/3.0
Fix nav links for login/logout/account
2015-02-28 02:53:13 -06:00
1ef47c8295 fix deleted new line 2015-02-28 18:51:30 +10:00
1f01036ba0 fix random d at end of line 2015-02-28 18:37:32 +10:00
bad0d5b5f7 fix nav again lol 2015-02-28 18:26:49 +10:00
11c92c9368 fix nav 3 2015-02-28 18:20:24 +10:00
5b9e51f70a fix nav 2 2015-02-28 18:13:58 +10:00
4ba38019d2 fixed nav.jade for login/register 2015-02-28 18:02:59 +10:00
Calvin Montgomery
8bee1afad1 Fix issue with channel settings not being updated 2015-02-27 13:10:38 -06:00
gv1222
8561d0418d Add a small padding to entire #messagebuffer 2015-02-25 14:20:07 -05:00
gv1222
5a3ad24a70 Fix form input color 2015-02-25 12:04:28 -05:00
gv1222
4b2d0019e2 Update form control color 2015-02-25 11:39:46 -05:00
gv1222
cf742cfc28 Remove poll background color 2015-02-25 11:29:49 -05:00
gv1222
dc7f9f41c7 Update guest login button 2015-02-25 10:56:56 -05:00
gv1222
7bb8f3f46f Minor tweaks and modern theme update 2015-02-25 10:56:45 -05:00
gv1222
45ca901f05 Transparency addition to modern theme 2015-02-25 10:56:35 -05:00
gv1222
e02726d052 Update modern bootstrap css. 2015-02-25 10:56:21 -05:00
gv1222
014d580d70 Align new poll and playlist buttons 2015-02-25 10:56:10 -05:00
calzoneman
a049a7e2dc Fix login/logout redirects 2015-02-24 11:08:10 -06:00
calzoneman
a0b7bff70c Fix 2015-02-24 10:48:51 -06:00
Calvin Montgomery
6ab609db71 Merge pull request #454 from calzoneman/gdocs-refactor
Gdocs refactor
2015-02-24 10:42:35 -06:00
Calvin Montgomery
62b81708ab Merge pull request #453 from calzoneman/csurf
Add csrf middleware
2015-02-24 10:42:11 -06:00
calzoneman
0dacf1019d Fix double encoding 2015-02-24 10:40:07 -06:00
Calvin Montgomery
f66b0993eb Merge pull request #450 from 6IRCNet/3.0
Drop root privledges
2015-02-24 10:34:00 -06:00
bush6
c2a00420f2 Merge remote-tracking branch 'upstream/3.0' into 3.0 2015-02-24 08:28:08 +10:00
bush6
ca0f0c4086 set uid timeout
Allow the timeout to be changed allowing more time before dropping root
privledges
2015-02-24 08:08:43 +10:00
calzoneman
afc0ea0a58 Add csrf prevention 2015-02-22 18:15:22 -06:00
Erik Little
420e77963b Merge pull request #452 from calzoneman/fixkick
Fix cancel for kick/ban/ipban
2015-02-21 21:36:59 -05:00
Erik
3ae2deca7c Fix cancel for kick/ban/ipban 2015-02-21 21:34:25 -05:00
calzoneman
400e15dea8 Fix logins on raw IPs in chrome 2015-02-21 14:48:24 -06:00
Calvin Montgomery
a88e8b8b7b Merge pull request #451 from Xaekai/3.0
Fix raw file links getting parsed as a non-existant ps: shorthand
2015-02-21 14:34:13 -06:00
Xaekai
1c263f825b Add line start anchors to the shorthand URI parsers 2015-02-21 12:26:03 -08:00
bush
960f94bfb6 Forgot the new file :o 2015-02-21 19:13:55 +11:00
bush
2f6fb43152 added a feature to change uid/gid after startup to bind ports lower than
1024 on Linux
2015-02-21 19:12:26 +11:00
calzoneman
a6eaa944c1 Fix improper null check 2015-02-20 23:23:10 -06:00
calzoneman
c9025fbb44 Fix titles and error messages 2015-02-20 23:17:34 -06:00
Xaekai
a291cfecb4 Merge remote-tracking branch 'upstream/3.0' into 3.0 2015-02-20 21:04:43 -08:00
calzoneman
ad13896739 Make gdocs retrieval less janky 2015-02-20 22:59:11 -06:00
calzoneman
9ab4e02a5d CSS fixes 2015-02-20 18:54:31 -06:00
calzoneman
1b5f8d47aa Fix 2015-02-20 18:54:00 -06:00
calzoneman
df62ee8d58 Fixes 2015-02-20 18:54:00 -06:00
calzoneman
08a9eae2d3 Change login sessions 2015-02-20 18:53:02 -06:00
calzoneman
b579db5310 Change login sessions 2015-02-20 18:53:02 -06:00
calzoneman
10aa7519da Only show quick login for large screens 2015-02-20 18:53:02 -06:00
calzoneman
a31273be5c Initial 'remember me' support for logins 2015-02-20 18:53:02 -06:00
Calvin Montgomery
cdcace828f Merge pull request #449 from 6IRCNet/3.0
Fix email from address and allow changing sender name
2015-02-19 15:23:44 -06:00
bush
ff8a9d02fe fixed missing , xD 2015-02-20 08:10:04 +11:00
bush
407f8930c3 Fixed email not getting sent from correct email in config. Added way to
change the default name (CyTube Services) that email is sent from.
2015-02-20 07:53:33 +11:00
Erik Little
bed10ec8a9 Merge pull request #448 from Xaekai/3.0
Resolve missing closing parenthesis or the generic matcher.
2015-02-18 21:46:07 -05:00
Xaekai
1f7940711d Resolve missing closing parenthesis on the generic matcher. 2015-02-18 18:44:17 -08:00
Xaekai
9a1baf64b6 Resolve missing closing parenthesis or the generic matcher. 2015-02-18 18:39:40 -08:00
Calvin Montgomery
f790a9dbd5 Merge pull request #446 from Xaekai/3.0
Enhance media link parser.
2015-02-17 10:42:33 -06:00
Xaekai
5c6a966e6f Use a generic matcher for the shorthand URIs. 2015-02-16 22:40:18 -08:00
Xaekai
ca17c82c8c Make doubly sure fixed calzoneman/sync#445 2015-02-16 00:50:15 -08:00
Xaekai
a636082500 Enhance media link parser.
Accept the shorthand URI style used in the logs as valid.
Add an underscore to the DailyMotion negated group to prevent dupe abuse.
2015-02-16 00:33:44 -08:00
Calvin Montgomery
8c33818b36 Merge pull request #439 from Xaekai/3.0
Google+ metadata retrieval overhaul
2015-02-16 00:29:32 -06:00
Xaekai
26a9446d3d I commit this code in the name of Pinkie Pie. 2015-02-15 17:36:29 -08:00
Xaekai
4ec61da811 Merge remote-tracking branch 'upstream/3.0' into 3.0 2015-02-15 04:25:43 -08:00
Xaekai
b909c93517 Merge remote-tracking branch 'upstream/3.0' into 3.0 2015-02-15 04:25:43 -08:00
Calvin Montgomery
af58b435ba Merge pull request #442 from Poniverse/feature/change-mysql-port
Added in ability to change mysql port
2015-02-14 16:39:38 -06:00
Adam Lavin
efb9d30de0 Added in ability to change mysql port 2015-02-14 22:20:26 +00:00
calzoneman
cc4d8514fa Fix connect message margin 2015-02-14 16:09:19 -06:00
calzoneman
9fea9089f4 Merge branch '3.0' of github.com:calzoneman/sync into 3.0 2015-02-14 13:07:35 -06:00
calzoneman
d61ba675dc Bump socket.io 2015-02-14 13:03:12 -06:00
calzoneman
406717bb18 Update to work on node v0.12 2015-02-14 12:17:33 -06:00
calzoneman
9938f2c605 Change all textbox keyup to keydown
Fixes an issue repoted by dragondread in IRC where a popup character map
was unintentionally triggering the keyup event after a character was
selected.
2015-02-13 13:40:58 -06:00
calzoneman
d7d3534d62 jquery cdn sucks 2015-02-10 23:07:57 -06:00
calzoneman
3bb3e020df More css fixes 2015-02-08 14:35:52 -06:00
calzoneman
7de62c1e84 CSS fixes 2015-02-07 21:17:06 -06:00
calzoneman
4e79c4cf49 bump socket.io (Automattic/socket.io#1980) 2015-02-06 00:41:21 -06:00
calzoneman
dfdab263a5 Fix safe nick for hover 2015-02-05 22:23:54 -06:00
Calvin Montgomery
5791803ca5 Merge pull request #434 from Xaekai/3.0
Userlist toggle changes
2015-02-05 12:27:36 -06:00
Xaekai
e3dcbabc96 Change userlist toggle chevron to point down in the channel jade because it's default state is open. 2015-02-05 10:01:51 -08:00
Xaekai
6eed208527 Correct chevron pull classing 2015-02-04 23:27:25 -08:00
Xaekai
0b2ae90d3f Change userlist chevron appearance on toggle.
Change userlist chevron placement for Synchtube layout
2015-02-04 23:14:51 -08:00
Calvin Montgomery
c8b1afffc0 Merge pull request #433 from gv1222/3.0
CSS Cleanup + Modern theme
2015-02-04 22:19:42 -06:00
gv1222
fea9f4adc0 Revert "run.sh"
This reverts commit 79046e697d.
2015-02-04 20:00:29 -05:00
Graham
72a68dfc5c Update footer color. 2015-02-02 21:53:17 -05:00
Graham
7bc16c7792 Cleanup queue_entry CSS. 2015-02-02 21:33:13 -05:00
Graham
ad9aa91072 Fix .drink colors. 2015-02-02 21:19:05 -05:00
Graham
0ba3b4ba78 Attempt to center chevron vertically. Some discrepancy between browsers. 2015-02-02 21:07:35 -05:00
Graham
ef50c527bc Revert stripes for compatibility with light themes. 2015-02-02 20:23:48 -05:00
Graham
5a9c0f98a4 Improve theme selection from modern theme 2015-02-02 20:02:45 -05:00
Graham
d1d8b1b748 Improve connect & disconnect messages for more cross-theme compatibility 2015-02-02 19:50:45 -05:00
Graham
3261613834 Revert "synctube.org index"
This reverts commit 15f1252bfa.
2015-02-02 16:49:40 -05:00
Graham
187112c734 Fix cdn links 2015-02-02 16:47:23 -05:00
Graham
5739d95ba6 Update theme with new modern design 2015-02-02 15:15:15 -05:00
Calvin Montgomery
93567c57f1 Merge pull request #432 from calzoneman/deps-upgrade
Upgrade dependencies
2015-01-31 11:44:38 -06:00
calzoneman
55d924cc56 Downgrade yamljs due to parsing problems 2015-01-28 00:56:04 -06:00
Calvin Montgomery
583e7e7616 Merge pull request #431 from calzoneman/hitbox
Add hitbox support
2015-01-27 00:29:44 -06:00
calzoneman
858207a6f8 Upgrade deps 2015-01-27 00:12:40 -06:00
Calvin Montgomery
bbd03e4e0f Log aliases when someone logs in 2015-01-26 12:20:19 -06:00
Graham
15f1252bfa synctube.org index 2015-01-25 12:21:52 -05:00
Graham
f28cde5c71 Improve profile css and general cleanup 2015-01-24 15:29:52 -05:00
Graham
5d488781ca Line up connect message and chat 2015-01-24 14:46:20 -05:00
Graham
461301abfc Merge branch '3.0' of https://github.com/calzoneman/sync into 3.0 2015-01-23 20:47:01 -05:00
Graham
219ab4b40a Minor fixes to modern theme. 2015-01-23 20:36:16 -05:00
calzoneman
5a95bacee4 Fix youtube...again 2015-01-22 23:34:39 -06:00
calzoneman
50bf876010 Add hitbox support 2015-01-22 23:21:31 -06:00
calzoneman
5cde74cbd4 Fix potential cause for playlist timer problem 2015-01-22 16:53:36 -06:00
calzoneman
7d2015620a socket.io: upgrade to 1.3 2015-01-19 17:43:22 -06:00
Graham
79046e697d run.sh 2015-01-19 10:33:53 -05:00
Graham
7c896d82fc Modern theme updates 2015-01-19 10:30:47 -05:00
calzoneman
9fc1cbd81c Whitelist <s> tags for filters 2015-01-19 01:26:46 -06:00
calzoneman
e76fd7b1c4 Fix client motd issue 2015-01-16 19:35:26 -06:00
calzoneman
4f3adef1d3 Revert to git based sanitize-html for package.json 2015-01-16 19:27:41 -06:00
Calvin Montgomery
d7ef0d1893 Merge pull request #428 from calzoneman/sanitize-html
Merge sanitize-html into 3.0 #yolo
2015-01-14 13:23:01 -06:00
calzoneman
139825168f Fix for private, but embeddable soundcloud tracks 2015-01-11 12:10:09 -06:00
calzoneman
991dc866b1 Merge branch 'sanitize-html' of github.com:calzoneman/sync into sanitize-html 2015-01-08 20:07:22 -06:00
calzoneman
80c4c90bcf Migrate old MOTDs and don't replace \n with <br> after 2015-01-08 20:07:02 -06:00
Calvin Montgomery
56d6eb8026 sanitize-html: replace github link with npm
The change I made to sanitize-html was merged and published to npm, so
there's no reason to depend on my fork anymore.
2015-01-08 20:07:02 -06:00
Calvin Montgomery
eca8014369 deps: update sanitize-html 2015-01-08 20:07:02 -06:00
Calvin Montgomery
12f3161f50 XSS: Glob attributes data-*, aria-* 2015-01-08 20:07:01 -06:00
Calvin Montgomery
1c3a669279 Replace XSS filter with sanitize-html 2015-01-08 20:07:01 -06:00
calzoneman
654573af68 Migrate old MOTDs and don't replace \n with <br> after 2015-01-08 20:06:41 -06:00
calzoneman
8630c5972c deps: upgrade socket.io to 1.2.1 2015-01-08 17:57:44 -06:00
Erik
4135ec0bf8 Kick/Mute immunity should only be if globalRank is strictly greater 2015-01-08 09:58:44 -05:00
Calvin Montgomery
032f600746 Kick/Mute immunity should only be if globalRank is strictly greater 2015-01-08 08:48:00 -05:00
Calvin Montgomery
ef7ee067f2 Merge branch '3.0' of github.com:calzoneman/sync into 3.0 2015-01-07 15:59:52 -05:00
Calvin Montgomery
c1ef0848cd Add CSS classes for muted users (#426)
- If a user is muted, the `userlist_muted` class is added to the
  corresponding `.userlist_item`.
- If the user is shadowmuted, the `userlist_smuted` and `userlist_muted`
  classes are added.
- If the user is AFK, the `userlist_afk` class is added.
2015-01-07 15:58:36 -05:00
Calvin Montgomery
21756d7cb2 sanitize-html: replace github link with npm
The change I made to sanitize-html was merged and published to npm, so
there's no reason to depend on my fork anymore.
2015-01-07 10:47:46 -05:00
Erik Little
7dab3276a6 Merge pull request #425 from calzoneman/muteadmin
site admins should be immune from kick/mute
2015-01-06 23:00:14 -05:00
Erik
5d843358d2 site admins should be immune from kick/mute 2015-01-06 22:55:14 -05:00
Calvin Montgomery
b56809138c deps: update cytubefilters commit hash 2015-01-06 22:40:49 -05:00
Calvin Montgomery
aabff5d0cc deps: update cytubefilters commit hash 2015-01-06 22:40:23 -05:00
Calvin Montgomery
6e053ae7f4 deps: update sanitize-html 2015-01-06 13:05:31 -05:00
Calvin Montgomery
03f58a7d7a XSS: Glob attributes data-*, aria-* 2015-01-06 13:00:36 -05:00
Calvin Montgomery
df42a5e6a6 Replace XSS filter with sanitize-html 2015-01-06 12:20:48 -05:00
Calvin Montgomery
cf35c92391 Apparently this happens a lot, don't put it in the logfile 2015-01-06 10:58:15 -05:00
Calvin Montgomery
414cbfdc5d Add more safeguards for socket errors 2015-01-06 10:54:14 -05:00
Calvin Montgomery
cd22570c40 Hopefully fix youtube setPlaybackQuality once and for all 2015-01-04 16:46:40 -05:00
Calvin Montgomery
3423f43f2f https://www.youtube.com/watch?v=9u6Bfnq3aZk 2015-01-03 21:36:58 -05:00
Graham
4aa1df8837 Modern theme update 2015-01-03 19:46:02 -05:00
Graham
207ad43140 Update css 2015-01-03 18:21:33 -05:00
Graham
fb1636552b Add modern theme 2015-01-03 18:21:07 -05:00
Calvin Montgomery
bf70d2760b Log when a video is added 2015-01-03 16:03:15 -05:00
Graham
32c58a9628 Minify slate and cleanup. 2015-01-03 15:32:15 -05:00
Graham
e4de90c38b Move color CSS rules into their own files. minify bootstrap 2015-01-03 15:27:15 -05:00
Calvin Montgomery
829cc090fa Use graceful-fs to maybe prevent EMFILE 2015-01-02 23:22:48 -05:00
Calvin Montgomery
4013779750 Remove license banner from index.js (see LICENSE file) 2014-12-31 12:07:43 -05:00
Calvin Montgomery
0c23b8a4c5 Update Copyright year; remove old junk 2014-12-31 12:06:29 -05:00
Calvin Montgomery
c5582865a5 Merge pull request #422 from calzoneman/cytubefilters
Switch to PCRE-based C++ chat filters
2014-12-28 20:40:29 -06:00
Calvin Montgomery
4319111f47 Remove console.log 2014-12-28 19:09:41 -05:00
Calvin Montgomery
058b24323d Add cytubefilters to package.json 2014-12-28 19:07:39 -05:00
Calvin Montgomery
25eba6ab2b Improve filter handling code 2014-12-28 11:12:37 -05:00
Calvin Montgomery
aa5e50f1d2 Cytubefilters, part 1 2014-12-27 01:39:30 -05:00
Calvin Montgomery
709724efd4 Warn moderators when a channel exceeds size limit
When the chandump is saved, the size of the file is checked.  If it is over the limit, moderators are displayed a message indicating that the channel is too large and they should remove extra playlist items, filters, and/or emotes.

This is a partial solution for #421.
2014-12-26 11:19:19 -05:00
Calvin Montgomery
3689aafe3b Fix all video adds getting stuck when one fails
Whenever a urlRetrieve() fails due to an unexpected error (ENOTFOUND, ETIMEDOUT, Socket hang up, etc.), the domain handler and the global exception handler would detect this and not crash the server, however the dirty internal state would somehow prevent future HTTP requests from completing successfully.

Removed domain usage since that feature is marked "unstable" and is rumored to be marked for deprecation in future versions of node.  Using the "error" event of the request object itself, which means errors are local in scope and won't pollute global state.  This should have been the solution originally, but when urlRetrieve() was written, I was not as familiar with node.
2014-12-26 10:39:47 -05:00
Calvin Montgomery
db7d1a22c8 Resolve #420 2014-12-19 14:39:10 -05:00
Calvin Montgomery
c39c394f36 Add auto DB conversion for utf8mb4 2014-12-14 21:53:25 -05:00
Calvin Montgomery
9deff9bdb1 Change charset for certain fields to utf8mb4
The underlying cause of #419 is the default utf8 collation in MySQL/MariaDB, which only supports the base plane of Unicode (\u0000-\uffff).  By changing the collation to utf8mb4_general_ci, stuff like ban reasons and profile text may have emoji and other non-base-plane Unicode.

The charset for playlist titles is NOT changed, and non-base-plane characters are replaced by question marks.  This is because switching to utf8mb4 would make the primary key too long.
2014-12-14 21:53:25 -05:00
Calvin Montgomery
4b8681c2a4 Don't break if the profile is corrupt 2014-12-14 21:53:25 -05:00
calzoneman
e8a2753e19 Don't log HTTP 413, just send it to the client and be done 2014-12-12 17:35:57 -06:00
calzoneman
a3a9fa074e Improve behavior of custom embed w.r.t. https
Instead of silently failing when browser policy blocks HTTP embeds over HTTPS, pre-fill the video div with an error message and attempt to salvage the link with s/http/https/g.
2014-12-10 23:56:17 -06:00
calzoneman
db56a8869d Fix #417 2014-12-07 13:42:18 -06:00
calzoneman
2b800f2a9a Hopefully fix typecheckedOn bug 2014-12-07 00:08:53 -06:00
calzoneman
cf60994895 Dailymotion is a completely functional site programmed by competent people 2014-12-04 15:55:15 -06:00
calzoneman
918b865a9b Slight tweak to urlRetrieve error handler 2014-12-04 00:42:25 -06:00
calzoneman
5cbdb47eb1 Merge branch '3.0' of github.com:calzoneman/sync into 3.0 2014-12-02 22:24:52 -06:00
calzoneman
b587da6701 Several fixes
- User playlists should now list correctly (fixed a race condition)
  - Livestream types can autoplay (no longer stuck at currentTime = -3)
  - Playlist items with NaN duration do not break user playlist saving
  - ffmpeg support can handle live media (e.g. icecast)
  - Invalid volume is sanitized and an error message is added
  - JWPlayer displays correctly for both HTML5 and Flash
  - JWPlayer volume synchronization is fixed
  - <audio> and <video> tags are scaled correctly with .embed-responsive-item
2014-12-02 22:21:52 -06:00
Calvin Montgomery
33294278ad Gracefully handle elements with null duration during user playlist save 2014-11-27 08:53:03 -06:00
calzoneman
b09346392e Slight tweak to nick hovers 2014-11-24 19:32:06 -06:00
calzoneman
2f2ed8aaf9 Fix Twitch not working on HTTPS and allow HTTPS channel page 2014-11-24 19:24:47 -06:00
calzoneman
9db35ba811 Fix PM bar clickthrough 2014-11-24 19:08:51 -06:00
calzoneman
214353acab Fix JWPlayer issue some Firefox users are experiencing 2014-11-22 01:01:32 -06:00
calzoneman
b37bc6b1a5 Fix obnoxious link colors in tables on cyborg theme 2014-11-21 16:58:22 -06:00
calzoneman
e0191a50f0 Fix #414; use CDNs for bootstrap,jquery 2014-11-20 23:10:40 -06:00
calzoneman
da2d461941 Fix caching and add gzip 2014-11-16 21:06:10 -06:00
calzoneman
610fd5a7c3 Handle #412 2014-11-16 11:19:14 -06:00
calzoneman
2f9b5ffe6a Fix misaligned checkbox in new poll menu 2014-11-16 11:15:08 -06:00
calzoneman
af4f0fd211 Fix chat disappearing if the video player is gone 2014-11-15 16:52:18 -06:00
Calvin Montgomery
43cc336e07 Merge pull request #410 from calzoneman/responsive-embed
Update Bootstrap to 3.3; improve automatic video/chat resizing with responsive-embed
2014-11-15 11:15:10 -06:00
calzoneman
08f9aeaedb JWPlayer fixes 2014-11-13 20:24:05 -06:00
calzoneman
4514151ad2 Add a better error message for google drive 1hr 2014-11-13 20:08:01 -06:00
calzoneman
7310dabedf CSS fixes 2014-11-12 19:56:29 -06:00
calzoneman
9f18a6978e Still WIP 2014-11-11 19:48:08 -06:00
calzoneman
7708cf1aca Resolve #408 2014-11-11 15:48:34 -06:00
calzoneman
2c45177cc0 Responsive embed, WIP 2014-11-10 22:43:49 -06:00
calzoneman
681fc717c3 Fix google+ 2014-11-03 12:56:15 -06:00
calzoneman
b49cd1b146 I'll take "hacks that shouldn't work" for 00 2014-11-01 12:12:22 -05:00
calzoneman
318a924b6d Extend blacklist to PMs 2014-10-29 15:07:57 -05:00
calzoneman
575b762ba1 Fix kick behavior 2014-10-25 22:49:22 -05:00
Calvin Montgomery
5d74559278 Merge pull request #406 from calzoneman/socket.io-1.0
Socket.io 1.0
2014-10-25 12:35:03 -05:00
calzoneman
77be5a46d4 I hate merge conflicts 2014-10-24 10:31:28 -05:00
calzoneman
4c5d441931 Minor update 2014-10-24 10:30:23 -05:00
calzoneman
3f62cd7dfb Socket.io pls 2014-10-24 10:24:02 -05:00
calzoneman
4967e46343 Fix socket.io issue; fix ACP announcements 2014-10-24 10:24:02 -05:00
calzoneman
289807535a Typo 2014-10-24 10:24:02 -05:00
calzoneman
b4bcb7637b Reject sockets with no IP 2014-10-24 10:24:02 -05:00
calzoneman
50dd0982a4 Fix IP extraction for socket.io 2014-10-24 10:24:02 -05:00
calzoneman
6e0735f3fe Update to socket.io 1.0 2014-10-24 10:23:30 -05:00
calzoneman
12804d1052 Add UI option for no emotes (#404) 2014-10-23 23:21:44 -05:00
calzoneman
029fa62af2 Don't log errors for bad http requests 2014-10-12 11:36:54 -05:00
calzoneman
fa85030524 Fix borrow-rank 2014-10-09 20:46:16 -05:00
calzoneman
d676abc722 Fix: pressing enter now submits login form on channel page 2014-10-08 22:36:33 -05:00
calzoneman
4505ca19da Auto unpause after leader is removed 2014-10-07 23:01:36 -05:00
calzoneman
e13e695077 Allow limiting # items per user 2014-10-06 11:32:25 -05:00
calzoneman
084b1cf16f Fix for super long pagetitles 2014-10-03 16:15:35 -05:00
calzoneman
309e5d8b46 Doing it live 2014-09-13 00:01:54 -05:00
calzoneman
d2027d2e5a Fix /unmute with empty name 2014-09-12 11:49:30 -05:00
calzoneman
731ab3f9a5 Ok this should actually fix it 2014-09-11 19:00:27 -05:00
calzoneman
2ea9dbcb75 Fix IP address race condition for logging 2014-09-11 18:56:33 -05:00
calzoneman
2b60ab8e10 Add permission for #402; fix a strange jwplayer issue 2014-09-06 22:59:28 -05:00
calzoneman
71114b0060 Fix: don't search channel library if rank < seeplaylist 2014-09-04 20:53:18 -05:00
calzoneman
bc3f20198c I'm not sure who to hate more 2014-09-02 17:30:41 -05:00
calzoneman
79d636ea89 Fix chrome incompatibility 2014-09-02 17:28:16 -05:00
calzoneman
293f943a9a Accept drive.google.com/file/d/blah as a substitute for docs.google.com/file/d/blah 2014-09-02 16:50:26 -05:00
calzoneman
ea9fa0a95a Fix /smute with 0 args throwing error 2014-09-02 16:46:53 -05:00
calzoneman
12447ce5dc Give up early if username is invalid for rank change 2014-09-01 20:33:11 -05:00
calzoneman
66865f718c Fix Error: Requested Range Not Satisfiable 2014-09-01 14:36:24 -05:00
calzoneman
91c24518c5 Conditionally allow ASCII characters (for Xaekai) 2014-08-29 16:38:57 -05:00
calzoneman
7002874bbb Minor fix for messages with spaces only 2014-08-29 15:49:32 -05:00
calzoneman
484b695965 Disallow ASCII control characters in messages 2014-08-29 15:47:56 -05:00
calzoneman
2a8b94e26a Chrome shiftclick workaround requested by Xaekai 2014-08-27 18:49:00 -05:00
calzoneman
7b00ba10b9 Fixes at Xaekai's request 2014-08-27 18:45:11 -05:00
calzoneman
ddb4be0c59 Socket.io pls 2014-08-24 13:38:34 -05:00
calzoneman
9be993a679 Fix socket.io issue; fix ACP announcements 2014-08-24 13:18:15 -05:00
calzoneman
c8684d58ed Fix handling of /mute with no name 2014-08-22 10:52:15 -05:00
calzoneman
53971af737 Fix remove video reconnect bug 2014-08-21 20:32:58 -05:00
calzoneman
ec9ee1d37f Wrap socket callbacks in try-catch, fix mediaUpdate remove video 2014-08-21 20:30:24 -05:00
calzoneman
b3c3ee93ce Hopefully fix an error case
I have no idea why it's possible for sock.handshake to be defined and then undefined
2014-08-21 14:37:40 -05:00
calzoneman
c757f62fc1 Typo 2014-08-20 12:11:46 -05:00
calzoneman
ab49eaab76 Reject sockets with no IP 2014-08-20 12:09:38 -05:00
calzoneman
3e53b30305 Fix IP extraction for socket.io 2014-08-20 11:44:37 -05:00
calzoneman
edac89b204 Fix x-forwarded-for resolution in http logging 2014-08-20 10:56:36 -05:00
calzoneman
db86d3918f Update to socket.io 1.0 2014-08-19 22:57:28 -05:00
Calvin Montgomery
daec397015 Merge pull request #400 from calzoneman/deps
Update dependencies
2014-08-19 22:28:32 -05:00
calzoneman
088c547522 Update dependencies - execute npm update && npm rebuild after pulling 2014-08-19 22:27:59 -05:00
calzoneman
ae42ac9c56 Remove ghost httpaccess.log file 2014-08-19 00:52:50 -05:00
calzoneman
6dfeab9657 Update rest of dependencies 2014-08-19 00:46:30 -05:00
calzoneman
4eb81a84d9 Update nodemailer, yamljs, q 2014-08-19 00:36:31 -05:00
calzoneman
c693e84a46 Update bcrypt and fix a deprecated reference in auth 2014-08-19 00:25:36 -05:00
calzoneman
3d6aaf0e1f Fix a bug with theme.js 2014-08-19 00:23:05 -05:00
calzoneman
020e2326b5 Replace own static and log functions with serve-static and morgan 2014-08-19 00:21:32 -05:00
calzoneman
5f7adc98ba Update express dependency 2014-08-19 00:07:24 -05:00
calzoneman
c2ba55ac0f Fail /kick, /ban if the target name is blank 2014-08-18 22:00:51 -05:00
calzoneman
aff20c3012 Use JWPlayer as flash fallback because it can actually synch 2014-08-18 12:00:26 -05:00
calzoneman
6452ea4ab4 Fix ban reason 2014-08-18 11:49:03 -05:00
Calvin Montgomery
5d624fa49f Merge pull request #396 from calzoneman/ipcloak
Change IP masking
2014-08-15 11:43:56 -05:00
calzoneman
0f2b93e5c5 Increment version number 2014-08-15 11:41:09 -05:00
calzoneman
3446eb5357 Add Google Docs checks for missing duration/title 2014-08-15 00:27:51 -05:00
calzoneman
b82583a79c Remove IP leak from ban log message 2014-08-14 22:07:06 -05:00
calzoneman
c255133a2b Fix wrong argument being passed 2014-08-14 22:02:58 -05:00
calzoneman
25ab4b62e5 Update cloaking function so the banlist looks right 2014-08-14 21:57:38 -05:00
calzoneman
722c55e0da Remove reference to maskIP 2014-08-14 21:50:34 -05:00
calzoneman
c5a52d2ce5 Fix wrong scope on ipList 2014-08-14 21:45:25 -05:00
calzoneman
6b9968a489 Remove torblocker dependency 2014-08-14 21:44:33 -05:00
calzoneman
8fddbc3e6e Add IP cloaking; make tor bans channel specific 2014-08-14 21:42:13 -05:00
calzoneman
ecca806a58 Fix an issue with quality selection 2014-08-14 16:28:44 -05:00
calzoneman
83ae835bed Allow modals to be wider on large screens 2014-08-13 14:01:21 -05:00
calzoneman
dc3efd94c8 Remove JustinTV support as it is no longer in service 2014-08-13 13:51:09 -05:00
calzoneman
735b2fcd07 Improve URL parsing 2014-08-13 13:49:32 -05:00
calzoneman
fae1609a50 Apparently redirector is https sometimes 2014-08-12 18:34:37 -05:00
calzoneman
936c75a062 Update user agreement to explicitly mention spamming other sites 2014-08-11 20:50:43 -05:00
calzoneman
b88f1931c6 Prevent vimeo from logging errors when videos become private/removed 2014-08-11 11:58:03 -05:00
Erik
ef921b1f96 Fix logging of customizations 2014-08-09 14:41:12 -04:00
Calvin Montgomery
ea79b7d6b1 Remove unused file 2014-08-08 23:29:07 -07:00
Calvin Montgomery
9ff23622a0 Fix a minor matching issue for google docs 2014-08-08 23:27:54 -07:00
Calvin Montgomery
4f7ec228d3 Fix regular expression for google docs parsing 2014-08-07 23:19:07 -07:00
Calvin Montgomery
f11be6ae81 Fix pausing on HTML5 player 2014-08-07 20:34:24 -07:00
Calvin Montgomery
4a3645e3ed Prevent invalid URI escapes from logging errors 2014-08-07 19:25:34 -07:00
Calvin Montgomery
ae4f159cf7 Merge pull request #389 from calzoneman/god_damnit_google_drive
Fix for google docs changing their video player
2014-08-06 20:21:08 -07:00
Calvin Montgomery
b7edfc31f9 Fix for google docs changing their video player: 2014-08-06 20:12:57 -07:00
Calvin Montgomery
d94c596063 Require auth for read-only requests too 2014-08-04 18:01:57 -07:00
Calvin Montgomery
032dede66d Soundcloud fix, part 2 2014-08-03 22:56:17 -07:00
Calvin Montgomery
7acfcaf152 Soundcloud changed their volume range silently
[raging intensifies]
2014-08-03 22:50:24 -07:00
Calvin Montgomery
59dd733219 Fix null reference bug 2014-08-02 19:15:49 -07:00
Calvin Montgomery
84a07030d0 Disallow cloning playlist if user doesn't have seeplaylist permission 2014-08-02 19:12:09 -07:00
Calvin Montgomery
952b2d66d3 General Settings should be selected by default in channel settings 2014-07-30 19:03:18 -07:00
Calvin Montgomery
a6d0ae5993 Merge pull request #387 from calzoneman/channeloptions
Group chat options under channel settings
2014-07-30 19:29:15 -05:00
Erik
2264013a3b Small change per calzone's request 2014-07-30 20:26:29 -04:00
Erik
ee4624fd73 tweak options layout 2014-07-30 20:13:25 -04:00
Calvin Montgomery
d9f5c551e3 Fix TypeError due to race condition 2014-07-20 19:36:29 -07:00
Calvin Montgomery
79bb6a96cd Fix exception 2014-07-14 21:13:45 -07:00
Calvin Montgomery
89de33031c Merge pull request #386 from calzoneman/plusvideo
Google+ video support
2014-07-13 22:16:00 -07:00
Calvin Montgomery
35c6a68e5e Remove redundant RegExp constructor 2014-07-13 22:14:37 -07:00
Calvin Montgomery
b1709758fd Increment version number 2014-07-13 22:13:16 -07:00
Calvin Montgomery
565e490078 Fix setFlag/waitFlag edge case that was causing duplicate joins 2014-07-13 12:23:04 -07:00
Calvin Montgomery
e6d6e3391e Don't log gdocs 'exceeded quota' errors to error.log 2014-07-13 11:33:10 -07:00
Calvin Montgomery
989a737ea4 s/e.trace/e.stack 2014-07-13 11:31:41 -07:00
Calvin Montgomery
69772bf2ec Error handling for google+ 2014-07-13 11:29:50 -07:00
Calvin Montgomery
1e38b05800 Fix minor issue 2014-07-13 11:15:56 -07:00
Calvin Montgomery
c213eb2f31 Minor fix 2014-07-12 18:39:48 -07:00
Calvin Montgomery
b6a1dd8cb3 Send onMediaChange (fixes voteskip stuff) 2014-07-11 21:09:28 -07:00
Calvin Montgomery
937ad04967 Change meta format so it persists in DB and on disk 2014-07-11 20:42:13 -07:00
Calvin Montgomery
8acffda8ec Add clientside support for Google+ 2014-07-10 23:23:48 -07:00
Calvin Montgomery
c522516b88 Implement serverside Google+ retrieval 2014-07-10 23:03:13 -07:00
Calvin Montgomery
052afaf2f6 Start working on google+ support 2014-07-10 20:10:00 -07:00
Calvin Montgomery
b28fd9e4a8 Add permission for /clear and log it 2014-07-10 20:03:47 -07:00
Calvin Montgomery
0e8e0aef54 Merge pull request #385 from calzoneman/playback-improvements
Improvements to code handling Vimeo hack, Google Docs
2014-07-10 19:55:31 -07:00
Calvin Montgomery
c7ef76c518 Minor change 2014-07-10 19:54:26 -07:00
Calvin Montgomery
0f11615a1f Increment version number 2014-07-10 19:52:16 -07:00
Calvin Montgomery
1c77a24839 Fix activeLock bug 2014-07-09 22:37:11 -07:00
Calvin Montgomery
f92d4bc5d4 Fix vimeo with no vimeo-workaround 2014-07-09 22:15:14 -07:00
Calvin Montgomery
2d0fe02a19 Move vimeo simulator out of the changemedia callback 2014-07-09 21:55:49 -07:00
Calvin Montgomery
f36d2b0258 Add onPreChangeMedia and improve refreshing 2014-07-09 21:46:45 -07:00
Calvin Montgomery
3f959087af Initial improvements to playback system 2014-07-09 21:20:14 -07:00
Calvin Montgomery
a97db09928 Shift+click workaround for Chrome on playlist 2014-07-07 21:48:23 -07:00
Calvin Montgomery
d3b4ac1468 Change script prompt to be more friendly 2014-07-01 21:13:52 -07:00
Calvin Montgomery
256a951531 Fix potential race condition 2014-07-01 21:09:20 -07:00
Calvin Montgomery
03ba657783 Fix setting channel js/css 2014-07-01 20:43:34 -07:00
Calvin Montgomery
002dadd67a Fix question marks breaking channel joins 2014-07-01 20:35:13 -07:00
Calvin Montgomery
ece32dda5d Fix chrome bug 2014-07-01 20:29:12 -07:00
Calvin Montgomery
e87ddb473b Require user permission to run channel js 2014-07-01 20:11:54 -07:00
Calvin Montgomery
3661ab1fd9 Update XSS filter 2014-06-25 20:22:54 -07:00
Calvin Montgomery
9ce02c8e6b Merge pull request #380 from calzoneman/tablemerge
Merge channel specific tables into global tables with an indexed channel column
2014-06-25 20:14:03 -07:00
Calvin Montgomery
3dcdaf3045 Update version number 2014-06-25 20:13:03 -07:00
Erik
817c7ad4ba quickfix 2014-06-25 16:13:54 -04:00
Calvin Montgomery
f44c9ce51b Fix updater 2014-06-24 20:28:04 -07:00
Calvin Montgomery
36c4e41131 Add console command for deleting old channel tables 2014-06-23 22:15:57 -07:00
Calvin Montgomery
c768d9595c Merge ban tables, fix channel create/delete operations 2014-06-23 22:10:15 -07:00
Calvin Montgomery
4afd69b2fb Merge channel ranks tables 2014-06-23 21:40:40 -07:00
Calvin Montgomery
0abaaba690 Merge channel libraries into a single table 2014-06-23 21:09:18 -07:00
Calvin Montgomery
523bdfe065 Handle #377 2014-06-17 19:11:41 -07:00
Erik
5b76d4fb8c fix voteskip not setting afk to false 2014-06-15 21:13:22 -04:00
Calvin Montgomery
8152008466 Crash fix 2014-06-15 16:34:32 -07:00
Calvin Montgomery
0102b92730 Fix wrong variable reference 2014-06-15 12:51:39 -07:00
Calvin Montgomery
8c50655ff2 Disable IP check for password recovery, resolves #376 2014-06-15 10:58:53 -07:00
Calvin Montgomery
81d16b09fa Update config.template.yaml 2014-06-12 20:31:22 -07:00
Calvin Montgomery
7f4e2a8882 Fix a few issues 2014-06-12 20:29:12 -07:00
Erik
6f737349db uncomment 2014-06-11 12:45:55 -04:00
Calvin Montgomery
b71d3610f2 Fix #374 2014-06-11 08:56:06 -07:00
Calvin Montgomery
f75ffe089c Toss out fluent-ffmpeg in favor of own parser 2014-06-08 21:03:29 -07:00
Calvin Montgomery
ac10f05f21 Update ffmpeg loader to work with newer fluent-ffmpeg; fix playlists 2014-06-07 21:25:48 -07:00
Calvin Montgomery
02771e6623 Add raw video/audio playback with ffmpeg 2014-06-07 16:57:25 -07:00
Calvin Montgomery
63e60e65f0 Fix meta clearing on playlist save 2014-06-07 13:00:23 -07:00
Calvin Montgomery
777a87a8e6 Fix meta clearing on playlist save 2014-06-07 12:59:42 -07:00
Calvin Montgomery
19aac29347 Fix large chandump notification 2014-06-07 12:56:12 -07:00
Calvin Montgomery
6adba2f355 Don't run the database updater if the version is higher than current 2014-06-07 10:46:44 -07:00
Calvin Montgomery
5d5bdfc069 Various fixes for raw file playback 2014-06-07 10:45:52 -07:00
Calvin Montgomery
2b525c13fb Fix double-logout bug with SSL 2014-06-07 00:20:52 -07:00
Calvin Montgomery
14c13f845e Merge conflict 2014-06-05 22:03:57 -07:00
Calvin Montgomery
6dde745784 Better error handling, add support for mp3/ogg-vorbis 2014-06-05 22:02:51 -07:00
Calvin Montgomery
b8ed36df5a Better error handling, add support for mp3/ogg-vorbis 2014-06-05 21:58:33 -07:00
Calvin Montgomery
fdf7900dc8 Send rank packet to target on rank change 2014-06-05 21:12:21 -07:00
Calvin Montgomery
1d1630fb50 Implement raw file queues 2014-06-03 21:21:00 -07:00
Calvin Montgomery
626725031f Fix IPv6 mask regex 2014-06-02 20:59:35 -07:00
Calvin Montgomery
ac7f0ac47a Add update function to add meta column 2014-06-02 20:47:21 -07:00
Calvin Montgomery
f2769e5062 Start adding file playback queue support 2014-06-01 11:43:18 -07:00
Calvin Montgomery
30d4e65061 Change RawVideoPlayer and FlashPlayer to FilePlayer 2014-06-01 10:54:53 -07:00
Calvin Montgomery
862a7d876d Fix #372 2014-05-31 22:13:53 -07:00
Erik
13f5e3a2c8 fix? #371 coming out of afk doesn't enable voteskip button if it's on the same media 2014-05-31 10:31:45 -04:00
Calvin Montgomery
b10a2af1ad Hopefully fix #371? 2014-05-29 20:44:09 -07:00
Calvin Montgomery
4d0149ce43 Add form-control CSS class to new dropdowns 2014-05-26 13:25:23 -07:00
Calvin Montgomery
8069378afc Improve chat highlight options per #369 2014-05-26 13:22:20 -07:00
Calvin Montgomery
1917baa4c3 Fix unregistered channels and jwplayer custom timing 2014-05-26 13:02:03 -07:00
Calvin Montgomery
3cae0b0e57 Prevent server from infinite looping when google docs refresh fails 2014-05-24 11:06:46 -07:00
Calvin Montgomery
f3eb999a76 Refactor channel packing 2014-05-23 23:09:36 -07:00
Calvin Montgomery
02ac983fba Start working on channel detail view 2014-05-23 22:40:35 -07:00
Calvin Montgomery
ecc5ffc7da Correct filename for channel loggers 2014-05-23 22:00:51 -07:00
Erik
af0ddd303d fix #363 2014-05-23 22:07:54 -04:00
Calvin Montgomery
ca5ad87414 Fix google docs autorefresh 2014-05-22 20:04:43 -07:00
Calvin Montgomery
738dd3fb75 Fix 'user voteskipped [object Object]
'
2014-05-22 19:51:35 -07:00
Calvin Montgomery
89c94701dc Fix #368 2014-05-22 19:50:26 -07:00
Calvin Montgomery
f3dae85b99 Add more useful error logging to google docs refresh 2014-05-22 19:36:01 -07:00
Calvin Montgomery
3994b42444 Add /kickanons; fix handling of kickban commands with no arguments 2014-05-22 19:34:46 -07:00
Erik
d004c0bcdd fix poll unvoting 2014-05-22 18:08:24 -04:00
Erik
baa8af58bc hopefully fix #366 delete causing change media 2014-05-22 10:28:58 -04:00
Erik
4f7e4ad65e fix #365 parsing media limit 2014-05-22 09:22:58 -04:00
Erik
d42fc2eabf fix typo 2014-05-22 02:37:04 -04:00
Erik
e19acd2d63 fix enforcement of media limit 2014-05-22 02:35:52 -04:00
Calvin Montgomery
72c8b4f4fb Update LICENSE 2014-05-21 21:38:53 -07:00
Calvin Montgomery
1b3d154055 Prevent empty channel from being loaded on blacklist join 2014-05-21 21:11:15 -07:00
Calvin Montgomery
4e7dcbe7ef Fix for soundcloud returning 302 found instead of 200 OK 2014-05-21 20:53:13 -07:00
Calvin Montgomery
4c1b8e8c1a Add ability to blacklist channels in site config 2014-05-21 20:33:24 -07:00
Calvin Montgomery
7dde8cffe9 Hopefully fix playlist issues 2014-05-21 20:27:49 -07:00
Calvin Montgomery
ac629d4b7f Clear poll timer on channel unload 2014-05-21 20:21:03 -07:00
Calvin Montgomery
a3469378aa Minor fix 2014-05-21 20:18:27 -07:00
Calvin Montgomery
68475e2b30 Fix #364 2014-05-21 20:10:14 -07:00
Calvin Montgomery
2ce2467f4d Fix #363 2014-05-21 20:09:10 -07:00
Calvin Montgomery
0c9099e766 Fix #362 2014-05-21 20:07:04 -07:00
Calvin Montgomery
46b288d6ee Fixes 2014-05-21 08:42:09 -07:00
Calvin Montgomery
b6dcbe4d46 Fix duplicate userLeave 2014-05-20 22:50:55 -07:00
Calvin Montgomery
d598675576 Send deleteChatFilter packet 2014-05-20 22:42:51 -07:00
Calvin Montgomery
da6eadf33b Fix regex warning message 2014-05-20 22:41:21 -07:00
Calvin Montgomery
f5528c766f only execute callback if it's not undefined 2014-05-20 22:32:26 -07:00
Calvin Montgomery
949299c5e5 Bump emotes above chat so that emotes will render in backlog 2014-05-20 22:28:23 -07:00
Calvin Montgomery
2be928eea1 Fix poll timers 2014-05-20 21:38:18 -07:00
Calvin Montgomery
8b9242fc7b Add failsafe for VOLUME=NaN 2014-05-20 21:10:08 -07:00
Calvin Montgomery
5f0d2db1be Add checks for dead channels 2014-05-20 20:59:36 -07:00
Calvin Montgomery
705b8ce10a Fix vimeo workaround 2014-05-20 20:56:42 -07:00
Calvin Montgomery
39090fbe40 Fix check for channel being registered 2014-05-20 20:48:19 -07:00
Calvin Montgomery
1cc769b1ea More missed merges 2014-05-20 20:13:41 -07:00
Calvin Montgomery
6046ea2480 Fix the wrong files being merged 2014-05-20 20:11:40 -07:00
Calvin Montgomery
d27a8efcb1 Remove debug database print message 2014-05-20 19:33:34 -07:00
Calvin Montgomery
9ea48f58cf Merge refactoring into 3.0 2014-05-20 19:30:14 -07:00
calzoneman
91bf6a5062 Fix #360 2014-05-16 23:29:14 -05:00
Calvin Montgomery
d16482b863 Make vimeo workaround respect default quality option 2014-05-16 00:46:30 -05:00
Calvin Montgomery
03a188e8f2 Improvements to Dailymotion player
* Hide the logo by default
    * Set the wmode based on the user's preference
    * Automatic quality as with youtube
2014-05-16 00:36:33 -05:00
calzoneman
89939682ce Fix possible XSS issue with chat filters 2014-05-13 01:02:38 -05:00
calzoneman
79f7e96921 Fix possible issue with youtube workaround 2014-05-10 03:01:09 -05:00
Calvin Montgomery
6c4168c675 Improve workaround for YouTube's shitty internal race conditions 2014-05-09 20:09:17 -05:00
calzoneman
3fcb855c35 Fix the regex warning message about parentheses 2014-05-04 14:22:13 -05:00
calzoneman
992c8d8da9 Fix a case where users could cause duplicate joins 2014-05-02 12:06:31 -05:00
calzoneman
842fd80e0e Don't catch dangling /edit on google drive links 2014-05-01 10:22:03 -05:00
calzoneman
d6d1cd4289 Fix formatURL and parseMediaLink for google drive 2014-04-30 20:40:25 -05:00
calzoneman
e9fc3cfaca Fix #356 2014-04-30 19:36:01 -05:00
calzoneman
c11d31e716 Resolve #352 2014-04-30 17:40:16 -05:00
calzoneman
2a507fcac5 Fix incorrect parsing of x-forwarded-for in webserver.ipForRequest 2014-04-17 00:03:32 -05:00
calzoneman
d4885951fb Resolve #343 2014-04-14 19:22:01 -05:00
calzoneman
71f9ab79ea Resolve #349 2014-04-14 19:13:54 -05:00
calzoneman
48e808c8ea Fix ACP reading wrong config key 2014-04-13 19:27:32 -05:00
calzoneman
64980bc293 Add warning as discussed in #347 2014-04-13 02:14:34 -05:00
calzoneman
cfe34112a4 Fix emote issues as discussed in #347 2014-04-13 02:06:28 -05:00
calzoneman
445d8be55d Fix #346 2014-04-12 21:54:17 -05:00
calzoneman
7c252eab2f Fix ENOENT for people with HTTPS disabled 2014-04-12 21:52:06 -05:00
calzoneman
85c62b1579 Add login log message 2014-04-12 12:26:14 -05:00
calzoneman
54226c0ca5 Fix race condition failing to properly log in user 2014-04-12 12:22:30 -05:00
Calvin Montgomery
60ba970c34 Merge pull request #345 from calzoneman/serverdefs
Change the way IPs/ports to bind are specified
2014-04-11 14:10:40 -05:00
calzoneman
3567087c48 Update config.template.yaml and server files for new listen syntax 2014-04-11 10:52:51 -05:00
calzoneman
fb0533bd94 Convert server definitions to be more flexible 2014-04-11 00:14:52 -05:00
calzoneman
04dbb3444b Fix a few memory leaks; add /gc console command
3 memory leaks were fixed
  - ipThrottle (due to the periodic cleaner clearing the wrong object...)
  - ipCount (shouldn't have leaked very much, but removing obsolete data is good practice)
  - lastguestlogin (again, shouldn't leak much, but should be cleared periodically anyways)
A new console command (i.e. from the terminal running node) was added: /gc
  - If the process is invoked as node --expose-gc index.js, /gc allows you to manually invoke the garbage collector
2014-04-10 21:54:46 -05:00
calzoneman
e973813718 Patch a memory leak caused by an earlier failsafe
A global object AllPlaylists was added back in v2 as a hardfix for an issue where playlists would continue to send updates after the channel was reloaded and the playlist object was obsolete.  This condition should no longer happen due to other fixes, so the only thing this object was doing was wasting memory.
2014-04-10 16:10:55 -05:00
calzoneman
324fa6c81a Fix a typo and an undefined variable case 2014-04-08 18:06:37 -05:00
calzoneman
5c9c096209 Merge branch '3.0' of github.com:calzoneman/sync into 3.0 2014-04-08 16:52:13 -05:00
calzoneman
8ebacdbe12 Add missing return statement 2014-04-08 16:51:47 -05:00
Calvin Montgomery
ee4fafcc6a Add missing default keys to config.js 2014-04-07 14:41:21 -05:00
calzoneman
d6650f19ae Address #343 2014-04-06 00:29:34 -05:00
Calvin Montgomery
a141e3dafb Merge pull request #342 from calzoneman/vimeo_oauth
Add support for Vimeo's OAuth API
2014-04-04 20:07:21 -05:00
Calvin Montgomery
15ebd294a6 Merge branch 'vimeo_oauth' of github.com:calzoneman/sync into vimeo_oauth
Conflicts:
	config.template.yaml
2014-04-04 20:07:01 -05:00
Calvin Montgomery
a2f2b1adc2 Add support for Vimeo's OAuth ("advanced") API
This allows for authenticated API requests.  Currently, the only reason
you would want to use this is to be able to add videos that are marked
private but still embeddable.
2014-04-04 20:06:16 -05:00
Calvin Montgomery
bdd4744b8c Add support for Vimeo's OAuth ("advanced") API
This allows for authenticated API requests.  Currently, the only reason
you would want to use this is to be able to add videos that are marked
private but still embeddable.
2014-04-04 20:03:34 -05:00
Calvin Montgomery
4577a2dbd5 Start working on vimeo advanced api support 2014-04-04 11:37:30 -05:00
Calvin Montgomery
92e05b96c8 Use correct io.domain for sioSource in acp 2014-04-04 01:09:48 -05:00
Calvin Montgomery
42e590c6fd Fix #340; add shadowchat option for moderators
The new option allows moderators to see what shadowmuted users are saying.  When enabled, messages from shadowmuted users will appear in a darker (or lighter, depending on theme) font and struck through.
2014-04-01 11:52:20 -05:00
calzoneman
37db972d86 Minor change 2014-03-29 18:46:53 -05:00
calzoneman
a484b6c6a1 Implement layout reversal as per #336
- compactLayout() now reverses the changes made by fluidLayout, synchtubeLayout, hdLayout
2014-03-29 16:57:53 -05:00
calzoneman
43be6402a0 Fix edge case of emotes directly succeeding one another in a message 2014-03-27 11:03:27 -05:00
calzoneman
de145d00c7 Fix incorrect logging of user IP in User.login 2014-03-27 10:14:26 -05:00
calzoneman
f482a4e3b8 Correct typo; didn't cause errors but best to fix it anyways 2014-03-27 10:11:50 -05:00
calzoneman
b82b5289f1 Fix isIPBanned 2014-03-25 17:22:48 -05:00
calzoneman
095da3040b Merge branch '3.0' of github.com:calzoneman/sync into 3.0 2014-03-23 21:43:32 -05:00
calzoneman
ae04c91031 Fix #337 2014-03-23 21:43:20 -05:00
Calvin Montgomery
594e1e09a1 Update README.md 2014-03-21 18:15:13 -05:00
calzoneman
1f7cf66367 Fix minor issue with preJoin patch 2014-03-21 09:26:41 -05:00
calzoneman
7134de4de5 Fix another instance where #335 could be exposed 2014-03-20 09:18:54 -05:00
calzoneman
a6434ec47f Fix #335 2014-03-20 09:17:19 -05:00
calzoneman
74fc6aa81b Fix #334 2014-03-19 22:20:33 -05:00
calzoneman
ed65f3a648 Fix #333 2014-03-19 22:17:57 -05:00
calzoneman
bd5e11c46a Fix empty channel issue 2014-03-17 16:43:47 -05:00
calzoneman
07819f0b14 Fix race condition 2014-03-16 21:22:41 -05:00
Calvin Montgomery
aac8f3c671 Fix config template for nodemailer 2014-03-11 21:28:26 -05:00
Calvin Montgomery
d7bcf85c1b Hopefully fix some background task issues 2014-03-09 22:36:39 -05:00
Calvin Montgomery
e841196570 Fix #332; add more helpful errors 2014-03-09 11:42:45 -05:00
Calvin Montgomery
5e152c8310 Fix #331 2014-03-09 11:24:57 -05:00
calzoneman
2211e4da9c Change Vimeo fallback player 2014-03-07 20:37:20 -06:00
calzoneman
214537efdb Fix AFK count in usercount breakdown hover 2014-03-06 15:39:59 -06:00
calzoneman
9bd984f66b Fix typo in CA file loading 2014-03-06 15:38:46 -06:00
calzoneman
398647974c Add cafile config key 2014-03-05 22:26:10 -06:00
calzoneman
5393734055 Allow name banning guests 2014-03-05 17:49:49 -06:00
calzoneman
0537d0b202 Add wiki link to footer 2014-03-05 11:22:48 -06:00
calzoneman
881f8be5c4 Fix copyright 2014-03-04 23:56:00 -06:00
calzoneman
690ea9dbde Merge branch '3.0' of github.com:calzoneman/sync into 3.0 2014-03-04 16:22:29 -06:00
calzoneman
b225e9ef50 Fix auto afk timer 2014-03-04 16:22:16 -06:00
calzoneman
127e91b9d5 Fix /mute 2014-03-04 16:20:37 -06:00
Calvin Montgomery
f992d625fb Fixes 2014-03-04 11:57:05 -06:00
calzoneman
379c121350 Fix urlRetrieve error handler 2014-03-03 17:55:55 -06:00
calzoneman
db5b407635 Fix IP Bans 2014-03-02 22:00:24 -06:00
calzoneman
f75b6b754d Minor tweak 2014-03-02 01:38:35 -06:00
calzoneman
5082c84cc6 Fix user playlist add next 2014-03-02 01:35:20 -06:00
calzoneman
4f0094652b Fix import script 2014-03-01 17:43:03 -06:00
calzoneman
341d17c7b9 Fixes 2014-03-01 17:37:59 -06:00
calzoneman
b96b488c22 Merge branch '3.0' of github.com:calzoneman/sync into 3.0 2014-02-28 16:21:08 -06:00
calzoneman
086c2ab501 Fix chat linewrap; fix nodemailer 2014-02-28 16:20:58 -06:00
Calvin Montgomery
c93bc97a94 Hide channel settings button in chatonly for non mods 2014-02-28 13:55:29 -06:00
Calvin Montgomery
6e1de10860 Fix unregistered channel permissions 2014-02-28 13:53:23 -06:00
Calvin Montgomery
930402a058 Fix queueFail stacking 2014-02-28 13:49:17 -06:00
calzoneman
8c32ce4020 Add no_h264 option 2014-02-28 09:43:48 -06:00
calzoneman
b6a1e05cd0 Fix passworded room login 2014-02-28 09:21:28 -06:00
calzoneman
d75c8dd230 Hopefully prevent more webserver crashes 2014-02-28 09:04:41 -06:00
calzoneman
5b793710c3 Fixes 2014-02-28 08:43:04 -06:00
calzoneman
0f4c29952f Minor fixes 2014-02-28 00:34:50 -06:00
calzoneman
96cfe0070d fix user dropdown 2014-02-28 00:23:41 -06:00
calzoneman
3da717adb9 Fixes 2014-02-28 00:09:20 -06:00
CyTube
97f682a022 Emergency fix 2014-02-28 06:47:06 +01:00
CyTube
0dfb6df8a7 Fix a few things 2014-02-28 06:38:22 +01:00
calzoneman
fd328d443d Minor fix 2014-02-27 20:51:48 -06:00
calzoneman
c44e70893b Shitty hack for cross-domain login cookies 2014-02-27 20:50:47 -06:00
calzoneman
ceab7dabf9 Merge branch '3.0' of github.com:calzoneman/sync into 3.0 2014-02-27 17:27:40 -06:00
calzoneman
0fdf064904 Fix linewrapping on profile-box 2014-02-27 17:26:49 -06:00
Calvin Montgomery
99e7a521ef Tweaks 2014-02-26 14:37:51 -06:00
Calvin Montgomery
4e2c6d486b Add /reload console command, change some internal config 2014-02-26 14:28:24 -06:00
calzoneman
2bd6a207ba Minor fixes to channel listing 2014-02-26 11:04:22 -06:00
calzoneman
1e2a158ae6 Add io.domain config key 2014-02-26 10:57:49 -06:00
calzoneman
93d5980f05 Fix cross-domain cookie issue 2014-02-26 10:50:59 -06:00
calzoneman
ced68d9304 Fix layout issues 2014-02-25 23:28:32 -06:00
calzoneman
5565dd49fd Add caching to minify 2014-02-24 18:32:54 -06:00
calzoneman
5d9a8a1a3f Fixes 2014-02-24 18:25:49 -06:00
calzoneman
cc241da5cf Static content caching 2014-02-23 23:27:07 -06:00
calzoneman
0f9c3b2dd1 Fix error on empty user-agent 2014-02-20 21:26:09 -06:00
calzoneman
15dc61a761 Fix livestream issue 2014-02-20 21:24:28 -06:00
calzoneman
644437ea42 Directory cleanup 2014-02-18 22:00:56 -06:00
calzoneman
c54915e940 Fixes 2014-02-18 21:56:54 -06:00
calzoneman
6e0f27f254 Ability to hide playlist 2014-02-17 19:06:49 -06:00
calzoneman
6292ca5f3f Client tweaks 2014-02-17 18:56:36 -06:00
calzoneman
8be16303a1 Fix setChannelRank 2014-02-17 18:51:19 -06:00
calzoneman
20e00af18d Fix filter leak 2014-02-16 21:40:26 -06:00
calzoneman
e5333056e1 Fix ip ban 2014-02-16 17:55:05 -06:00
calzoneman
f39e051699 Add chatOnly and removeVideo 2014-02-16 17:54:33 -06:00
calzoneman
91aaed96fa Add socket.io login handler 2014-02-16 13:27:01 -06:00
calzoneman
54102863ac Fix random playlist death bug 2014-02-16 13:12:49 -06:00
calzoneman
b37893176f Fix error message placement 2014-02-16 02:19:51 -06:00
calzoneman
02dd57dad0 Fix casing issue 2014-02-16 02:16:58 -06:00
calzoneman
c9d4efdd15 Fix setMotd 2014-02-16 02:13:53 -06:00
calzoneman
39e26e8afd Give #pmbar a high zindex 2014-02-16 01:34:42 -06:00
calzoneman
7711587f15 Fix emotes 2014-02-16 01:33:38 -06:00
calzoneman
e8daf33146 Hopefully fix cross-domain login issue 2014-02-15 23:17:31 -06:00
calzoneman
638333c345 Fix opening extra tabs for uppercase names 2014-02-15 22:44:05 -06:00
calzoneman
bf959429f9 Fix case sensitivity of PMs 2014-02-15 22:23:18 -06:00
calzoneman
f7bf915f87 Fix google drive links with \xkk in them 2014-02-15 17:33:39 -06:00
calzoneman
5a4b18a1e7 A few fixes 2014-02-15 12:43:10 -06:00
calzoneman
ad30e3a805 Add some safety checks to PMs 2014-02-15 12:29:05 -06:00
calzoneman
b41529d4aa Add private messaging 2014-02-15 01:40:14 -06:00
calzoneman
573e59680e Fix a few things 2014-02-15 00:12:11 -06:00
calzoneman
1cbb1c2a6a Add contact page 2014-02-13 18:15:22 -06:00
calzoneman
d24214949a Add user agreement 2014-02-13 00:12:17 -06:00
calzoneman
27834e1211 ACP stats 2014-02-12 23:52:38 -06:00
calzoneman
002888a0de Implement emotes 2014-02-12 23:33:42 -06:00
calzoneman
53138fe1f0 Start working on emotes 2014-02-09 23:53:46 -06:00
calzoneman
0f9bfe1429 JWPlayer serverside synchronization 2014-02-09 20:10:11 -06:00
calzoneman
55b6e99896 Limit user registrations 2014-02-09 19:52:24 -06:00
calzoneman
23acdd7613 Slight template tweak 2014-02-09 00:25:24 -06:00
calzoneman
bf3832fb3a add checkboxes to add as temporary 2014-02-09 00:24:20 -06:00
calzoneman
cec68d0f2a Add poll timers 2014-02-08 23:58:27 -06:00
calzoneman
3bebc34e21 Redo channel logs 2014-02-08 12:45:07 -06:00
calzoneman
87b40b679a Refactor database tables init; make 2.x import script 2014-02-08 00:55:45 -06:00
calzoneman
0a480515d7 ACP event log 2014-02-07 10:45:28 -06:00
calzoneman
afa17165f7 Add ACP list active channels 2014-02-06 23:31:47 -06:00
calzoneman
359a228d5f Theme fixes; layout fixes; limit channel registrations 2014-02-06 10:37:00 -06:00
calzoneman
07feb91cc6 Make /account/ redirect to /login 2014-02-05 18:08:20 -06:00
calzoneman
767e90a757 Add config keys for reserved names 2014-02-05 18:05:52 -06:00
calzoneman
0998e89f5d Add channels ACP interface 2014-02-05 17:44:37 -06:00
calzoneman
bde57973c3 Finish users interface on ACP 2014-02-04 22:39:13 -06:00
calzoneman
7b9162f890 Fix vimeo workaround 2014-02-04 22:02:27 -06:00
Calvin Montgomery
12596cb357 Delete accidentally committed and irrelevant file 2014-02-04 22:00:08 -06:00
Calvin Montgomery
6fe31b9a3e Add configuration option to use express-minify for CSS and JS 2014-02-04 11:32:52 -06:00
calzoneman
e6acf92bdb Add volume normalization and vimeo workaround 2014-02-02 20:04:50 -06:00
calzoneman
1864cc0b35 Change the way unregistered channels work 2014-02-02 15:50:05 -06:00
calzoneman
b214c07fe0 Work on unregistered channels; fixes 2014-02-02 12:41:41 -06:00
calzoneman
0603a02d2e Finish password recovery 2014-02-01 13:03:08 -06:00
calzoneman
9562bc3757 Minor tweak to tab complete 2014-02-01 12:42:49 -06:00
calzoneman
2c6edb38b8 Persist announcements in the database 2014-02-01 12:41:06 -06:00
calzoneman
6498f6431b Fix tab completion of names 2014-01-31 11:01:51 -06:00
calzoneman
b762bb3747 Slight fix to tabcomplete 2014-01-30 23:17:19 -06:00
calzoneman
6051dd3939 A few UI fixes; improve tab complete algorithm 2014-01-30 23:02:58 -06:00
calzoneman
6f3b10222f Fix password box being not the same color on dark themes 2014-01-29 22:56:55 -06:00
calzoneman
156cc4f5f7 Fix glyphicons in dark themes on firefox 2014-01-29 22:55:50 -06:00
calzoneman
d410b4663d Implement new themes; fixes 2014-01-29 22:50:14 -06:00
calzoneman
ac89c87e29 Continue working on acp 2014-01-29 21:50:45 -06:00
calzoneman
1272425205 Work on ACP 2014-01-28 20:04:25 -06:00
calzoneman
d0be588149 Start working on ACP 2014-01-28 00:05:14 -06:00
calzoneman
5f3fa8922d Start working on event log 2014-01-27 18:37:48 -06:00
calzoneman
447a70be98 Add template for database update script, work on password recovery 2014-01-27 18:23:31 -06:00
calzoneman
8b0c370ad0 Fix leader issue 2014-01-27 16:44:22 -06:00
calzoneman
2018f3751f Small fix for HD layout 2014-01-26 14:18:29 -06:00
calzoneman
8a7cbb2a84 Add synchtube-fluid and hd layout 2014-01-26 14:15:50 -06:00
calzoneman
0f82faaef8 More fixes 2014-01-26 00:13:33 -06:00
calzoneman
6570c3da6c Fix a buttload of things 2014-01-26 00:01:36 -06:00
calzoneman
bedf3afb61 Fix fluid layout 2014-01-25 21:48:57 -06:00
calzoneman
574ef4435c Fix some login race conditions/issues 2014-01-25 21:29:56 -06:00
calzoneman
c3035ca368 Fix poll bug 2014-01-25 19:27:01 -06:00
calzoneman
cae52ef86f Fix profile hover box position 2014-01-25 18:12:49 -06:00
calzoneman
1355455548 Address race condition 2014-01-25 17:47:38 -06:00
calzoneman
0329e564a9 Fix passwords (hopefully) 2014-01-25 17:44:32 -06:00
calzoneman
e33c6c7860 Fix register 2014-01-25 16:59:25 -06:00
calzoneman
63055c51a2 Change login timeout 2014-01-25 16:39:16 -06:00
calzoneman
8313e7b006 Add HTML template config, fixes to account profile page 2014-01-25 13:55:00 -06:00
calzoneman
e70be5df42 Start working on theming 2014-01-25 13:49:34 -06:00
calzoneman
63ed9c7883 Continue work on password reset/recovery 2014-01-24 11:20:16 -06:00
calzoneman
65ef082a64 SSL fixes; work on password reset 2014-01-23 22:59:08 -06:00
calzoneman
21af0af1be Add new permission nodes 2014-01-23 16:03:50 -06:00
calzoneman
6e2d9c3caa Fixes 2014-01-23 15:53:53 -06:00
calzoneman
551d5b2c36 Add XSS filter 2014-01-23 11:45:08 -06:00
calzoneman
feca68538e Tweak animations 2014-01-22 22:43:17 -06:00
calzoneman
feabf35714 Work on SIO and SSL 2014-01-22 21:12:43 -06:00
calzoneman
4a2366eb06 Switch config to YAML 2014-01-22 17:11:26 -06:00
calzoneman
0a2dd6cbbe Update package.json 2014-01-21 23:29:26 -06:00
calzoneman
4f52f48664 Update bootstrap 2014-01-21 23:04:06 -06:00
calzoneman
0e2037f308 Tweak playlist and search UI 2014-01-21 22:41:53 -06:00
calzoneman
7307c9c82e Work on index page 2014-01-20 17:52:36 -06:00
calzoneman
24fcce3f87 Add prompt for kick/ban reason 2014-01-20 17:35:55 -06:00
calzoneman
e075d2f95a Add profile page, fix some redirects 2014-01-20 12:42:20 -06:00
calzoneman
fd6b95920a Add permissions editor 2014-01-20 12:16:30 -06:00
calzoneman
8c47221a22 Add quick mute buttons, fix delete not updating playlist meta 2014-01-19 16:50:14 -06:00
calzoneman
cd73653451 Handle mute/unmute/smute and related icons 2014-01-19 01:58:35 -06:00
calzoneman
6471969f55 Improve the way chat filter imports are handled 2014-01-19 01:45:20 -06:00
calzoneman
9c989f7ed7 Work on chat filters, UI stuff 2014-01-18 20:18:00 -06:00
calzoneman
7b18caa51c Change the way login redirect works 2014-01-16 16:23:55 -06:00
calzoneman
6a4031c188 Work on chat filters interface 2014-01-16 16:20:08 -06:00
calzoneman
24781df78f Work on channel settings 2014-01-16 11:53:34 -06:00
calzoneman
8aa92f73ec Work on channel settings 2014-01-15 00:16:29 -06:00
calzoneman
b1e6f696e8 Various fixes 2014-01-14 00:52:56 -06:00
calzoneman
87f44b69e0 Start reworking client UI 2014-01-13 18:31:12 -06:00
calzoneman
0a087c6507 Fix chat commands, fix a few bugs 2014-01-12 17:06:25 -06:00
calzoneman
637ece4044 Work on banlist 2014-01-11 23:55:52 -06:00
calzoneman
501a22556a Work on banlist 2014-01-09 17:43:07 -06:00
calzoneman
8554c38c7d Finish channel ranks / moderator list 2014-01-09 17:16:09 -06:00
calzoneman
2fadd70297 Work on user ranks 2014-01-08 23:45:26 -06:00
calzoneman
9306200a87 Work on ban list and channel ranks 2014-01-08 20:12:02 -06:00
calzoneman
2d7b0fe2ac Fix paginator 2014-01-08 10:57:48 -06:00
calzoneman
a4260bd25b Continue fixing things 2014-01-08 10:52:00 -06:00
calzoneman
22c4e8f9ff Fix more things 2014-01-07 22:47:00 -06:00
calzoneman
1aa464caa5 Continue fixing things 2014-01-06 09:55:12 -06:00
calzoneman
521c786cdc Start the long process of making it work again 2014-01-05 19:42:09 -06:00
calzoneman
b5a45a25bf Refactor user.js 2014-01-05 19:30:45 -06:00
calzoneman
1c79024984 Finish most of the channel.js rewrite 2014-01-04 23:15:54 -06:00
calzoneman
89cca0d552 continue working on channel.js 2014-01-01 22:18:33 -05:00
calzoneman
1b3199a4ef Continue working on channel.js 2013-12-31 15:12:47 -05:00
calzoneman
01aab965ad Continue work on channel.js 2013-12-27 21:06:10 -05:00
calzoneman
9611f86d55 Work on channel.js 2013-12-27 16:38:06 -05:00
calzoneman
ba0664641e Start rewriting channel.js 2013-12-27 11:08:03 -05:00
calzoneman
e27667b6d2 Fix channel registration rank from /account/channels 2013-12-26 23:53:43 -05:00
calzoneman
ead38a9d35 Work on account channels interface; Start work on /account/profile 2013-12-26 23:38:35 -05:00
calzoneman
5fe5dd440e Fix typos in channel.jade; fix username regex 2013-12-26 22:35:47 -05:00
calzoneman
c213dd1374 Initialize global database tables 2013-12-26 22:15:54 -05:00
calzoneman
aff737b3a1 Update changelog 2013-12-26 21:54:35 -05:00
calzoneman
6ac64a6cf3 Fix a few things 2013-12-26 10:17:33 -05:00
calzoneman
423c787ccb Fix registration 2013-12-25 22:30:24 -05:00
calzoneman
b5812081b8 Fix jade mixin syntax 2013-12-25 22:15:51 -05:00
calzoneman
bb531be7ca Fix jade doctypes 2013-12-25 22:04:26 -05:00
calzoneman
9d49df6157 Work on user options 2013-12-25 16:18:21 -05:00
calzoneman
fc63191773 continue work 2013-12-19 22:33:24 -05:00
calzoneman
556f9eb9e7 Continue working on client 2013-12-19 12:14:48 -05:00
calzoneman
afff414aad Continue work on clientside 2013-12-18 23:50:19 -05:00
calzoneman
5c40a72d47 Start refactoring clientside 2013-12-14 21:59:47 -06:00
calzoneman
47af1d4892 More database refactoring 2013-12-13 20:39:21 -06:00
calzoneman
fe00fb8c83 Refuctor channel related db functions 2013-12-13 18:52:13 -06:00
calzoneman
db5dcf86f7 Start refactoring database channels 2013-12-13 11:18:50 -06:00
calzoneman
a14363a845 More refactoring 2013-12-12 17:09:49 -06:00
calzoneman
cfd1b0618d Do a bit of intermediate database work 2013-12-12 16:28:30 -06:00
calzoneman
b889f7b4c8 Start merging cytube3 account management 2013-12-12 14:48:23 -06:00
calzoneman
8d2587cebd Implement get/setProfile 2013-12-12 14:27:18 -06:00
calzoneman
a821498298 Begin the long process of refactoring the database 2013-12-11 22:20:21 -06:00
calzoneman
353205b63a Update .gitignore 2013-12-11 16:58:21 -06:00
calzoneman
65ce2f661d Fix a slight bug with passworded rooms 2013-12-11 14:30:53 -06:00
Calvin Montgomery
b15c5654c5 Merge pull request #317 from nuclearace/patch-1
fix typo
2013-12-07 15:46:47 -08:00
nuclearace
57d4699fd3 fix typo
fix typo
2013-12-07 18:30:34 -05:00
calzoneman
fb355272fc Fix #316 2013-12-06 15:21:32 -06:00
Calvin Montgomery
c00ce26d57 Merge pull request #315 from calzoneman/xss
Minor correction to xss.js
2013-12-05 19:34:01 -08:00
Calvin Montgomery
1a44d7ca31 Merge pull request #314 from calzoneman/dev
Dev
2013-12-05 19:32:56 -08:00
calzoneman
3ced278bd0 Support links in polls 2013-12-01 17:20:42 -06:00
calzoneman
6cf2c40240 add extra user input validation and truncation 2013-12-01 17:10:37 -06:00
calzoneman
20ca0b5e1b Add some soft limits to user.js 2013-11-30 10:50:02 -06:00
Calvin Montgomery
d0544a8eb8 Merge pull request #313 from calzoneman/roompassword
Add channel passwords
2013-11-30 08:37:52 -08:00
calzoneman
2a84eab386 merge master into roompassword 2013-11-29 22:53:08 -06:00
calzoneman
3f1e665922 Add strict type checking to channel options 2013-11-29 21:21:49 -06:00
calzoneman
873330e991 Fix #312 2013-11-29 21:09:19 -06:00
calzoneman
cb4a931c30 Treat passworded channels as private 2013-11-26 11:16:04 -06:00
calzoneman
81b5c0eeaa Fix bug with userJoin, add password support to channel API 2013-11-26 11:06:10 -06:00
calzoneman
d006099fc7 Start working on room passwords 2013-11-25 16:20:15 -06:00
calzoneman
0945fc0d7b Clean up loadDump 2013-11-24 23:20:41 -06:00
calzoneman
01464ed394 Fix a few minor express issues 2013-11-24 18:13:58 -06:00
Calvin Montgomery
7f3561bd27 Merge pull request #311 from calzoneman/chatthrottle
Chat Throttle
2013-11-21 15:51:07 -08:00
calzoneman
714d302cf9 Automatically calculate cooldown 2013-11-21 17:50:17 -06:00
calzoneman
2f8b304b68 Fix /m and path traversal ActionLog 2013-11-21 17:46:33 -06:00
calzoneman
7868b4ce3e Fix linewrap on long afk names 2013-11-21 00:25:01 -06:00
calzoneman
4f821747ba Fix paginator bug 2013-11-21 00:08:49 -06:00
calzoneman
d36dcc5352 Exclude mods from chat throttle, add extra description to options 2013-11-20 10:14:39 -06:00
calzoneman
85e413bcdf Fix issues with chat logging after chat format refactoring
- Fix '<username.undefined>' in log
- Fix channel log filter
2013-11-20 10:10:01 -06:00
calzoneman
dfa454618a Fix that one YouTube issue (what the hell?) 2013-11-19 23:45:39 -06:00
calzoneman
b50dc3a626 Fix greentext bug 2013-11-19 16:14:33 -06:00
calzoneman
ee9b19b0ff Start switching chat flood system 2013-11-19 15:14:40 -06:00
calzoneman
21bb2b9a4e Backwards compatibility for formatChatMessage 2013-11-19 13:45:24 -06:00
Calvin Montgomery
a232b96786 Merge pull request #310 from calzoneman/commandrefactor
Refactor command system
2013-11-19 11:29:16 -08:00
calzoneman
2361ecddae Fix permissions on /a 2013-11-19 13:19:51 -06:00
calzoneman
b250f3c64b A few tweaks, cleanup 2013-11-18 11:11:00 -06:00
calzoneman
0e95ef2aa4 Continue refactoring 2013-11-17 15:32:19 -06:00
calzoneman
6e99990ef0 Start refactoring commands 2013-11-17 13:12:56 -06:00
Calvin Montgomery
b200d6cc2c Merge pull request #308 from calzoneman/logfilter
Logfilter
2013-11-16 10:59:36 -08:00
calzoneman
b077f3f206 Improve behavior 2013-11-16 00:00:03 -06:00
calzoneman
45ad9e44a3 Re-filter on updatE 2013-11-15 23:56:25 -06:00
calzoneman
c14581135e Fix the fix 2013-11-15 23:55:35 -06:00
calzoneman
ac15c17981 Fix log not updating 2013-11-15 23:54:45 -06:00
calzoneman
fc41b01209 Fix past commit 2013-11-15 23:47:32 -06:00
calzoneman
a008c923b7 Implement log filtering 2013-11-15 23:44:53 -06:00
Calvin Montgomery
ca4090c533 Start working on filter checkboxes 2013-11-15 20:23:57 -06:00
calzoneman
681911ffb1 Fix #307 2013-11-15 10:24:43 -06:00
calzoneman
22e8e10680 Fix being kicked when removing leader 2013-11-14 22:23:33 -06:00
calzoneman
8eef96770f Fix #306 2013-11-14 19:50:17 -06:00
calzoneman
f8e4a3fc34 Fix exception handler exception 2013-11-14 16:44:40 -06:00
calzoneman
d09d7ad64e Add an in-place MOTD editor 2013-11-13 22:36:43 -06:00
calzoneman
436df375c7 Fix #305 2013-11-10 22:26:30 -06:00
Calvin Montgomery
22e2180790 Merge pull request #304 from calzoneman/userupdaterefactor
Refactor broadcastUserUpdate
2013-11-09 10:43:04 -08:00
calzoneman
b6f4702570 Continue refactoring, tweak sban 2013-11-09 12:33:18 -06:00
calzoneman
d7a0297a8b Resolve merge conflict 2013-11-09 01:56:28 -06:00
calzoneman
30c5f67d4d Start refactoring leader and rank 2013-11-08 22:12:17 -06:00
calzoneman
7fc50db879 Add setLeader callback 2013-11-08 21:46:30 -06:00
calzoneman
6588e90bfe Add basic shadow mute command 2013-11-08 20:45:59 -06:00
calzoneman
e737bc9f8a Strip ip, expire from Google Docs flashvars 2013-11-08 17:57:14 -06:00
calzoneman
181abfeda0 Merge branch 'master' of github.com:calzoneman/sync 2013-11-07 19:01:41 -06:00
calzoneman
488a20e73a Forgot to commit changelog 2013-11-07 19:01:32 -06:00
Calvin Montgomery
6d6233cb79 Update README.md 2013-11-07 17:21:13 -06:00
calzoneman
4198f3ce2c Add support for Google Docs videos 2013-11-07 17:19:36 -06:00
calzoneman
2730c54344 fix #303 2013-11-06 17:43:12 -06:00
calzoneman
9e56400f53 Prevent double userLeave 2013-11-05 22:45:11 -06:00
calzoneman
0db5f64b15 Merge branch 'master' of github.com:calzoneman/sync 2013-11-05 22:40:09 -06:00
calzoneman
22ba96b9fd Prevent registration race condition 2013-11-05 22:39:51 -06:00
Calvin Montgomery
7318200878 Work on text filter 2013-11-05 10:37:50 -06:00
calzoneman
33d1075d44 Update changelog 2013-11-04 16:16:59 -06:00
calzoneman
ff2b2b43ce Merge branch 'master' of github.com:calzoneman/sync 2013-11-04 16:14:06 -06:00
calzoneman
241b7d1496 Fix version.py 2013-11-04 16:13:49 -06:00
calzoneman
760e14ca01 Fix package.json 2013-11-04 16:13:49 -06:00
calzoneman
ca4970d7eb Fix version.py 2013-11-04 16:13:27 -06:00
calzoneman
f4c05b736b Minor fix to tor blocker 2013-11-04 16:08:00 -06:00
calzoneman
4ad22308a0 Add module to block Tor IPs 2013-11-04 16:04:24 -06:00
Calvin Montgomery
9dc964bd77 Minor correction to xss.js 2013-11-02 02:28:43 -05:00
calzoneman
d06ca9a99f Comment xss.js 2013-11-02 02:25:16 -05:00
calzoneman
2e8f31ae2b Start working on text sanitizer 2013-10-31 18:53:03 -05:00
calzoneman
899c62f0b4 Fix package.json 2013-10-31 18:12:20 -05:00
calzoneman
1c3273978b Fix a few edge cases for XSS 2013-10-31 00:48:01 -05:00
calzoneman
271a23cdad Implement basic XSS filter 2013-10-31 00:39:35 -05:00
calzoneman
bf014530f9 Fix #302 2013-10-30 19:29:55 -05:00
calzoneman
1939314d72 Fix #301 2013-10-28 21:49:22 -05:00
calzoneman
f9c4685948 A couple special cases for dailymotion (Fix #300) 2013-10-24 17:31:04 -05:00
calzoneman
bc7a2e0ff1 Fix channel dead race condition (another one) 2013-10-22 13:42:07 -05:00
calzoneman
33e5f2e056 Fix MOTD XSS filter stripping style tags 2013-10-20 20:04:09 -05:00
calzoneman
9be513221e Remove some unnecessary packets 2013-10-19 12:20:55 -05:00
calzoneman
ad6b07357f Change the place where queuefail errors appear 2013-10-16 23:31:48 -05:00
calzoneman
152e46ab15 Add errDialog function 2013-10-16 23:22:37 -05:00
calzoneman
f8fcc0d2d7 Slight tweak to userlist dropdown hiding 2013-10-16 23:10:59 -05:00
calzoneman
033fbbf08a Hack around YouTube HTML5 player race condition -- Really guys? 2013-10-16 21:50:31 -05:00
calzoneman
120d56d6c8 Change the way /mute works 2013-10-16 17:36:05 -05:00
calzoneman
5eda748fc4 Be nicer about who is kicked 2013-10-16 17:21:11 -05:00
calzoneman
95c2118740 Make it impossible to ever lose rank when logging in 2013-10-14 18:15:36 -05:00
calzoneman
2e77cb4499 Change channel dumping to a single interval rather than per-channel 2013-10-14 16:39:41 -05:00
calzoneman
9193423923 Rate-limit socket.io connections per IP 2013-10-14 16:32:03 -05:00
calzoneman
b70526db4d Fix video deletes 2013-10-12 19:46:39 -05:00
calzoneman
8c80088192 Fix bugs 2013-10-12 19:44:36 -05:00
calzoneman
6c7566e790 Actually fix /poll 2013-10-12 19:04:42 -05:00
calzoneman
5c0d70f8eb Fix /poll 2013-10-12 19:03:39 -05:00
calzoneman
2744b3a20d Improve strictness of data checking 2013-10-12 18:59:50 -05:00
calzoneman
51d89b99e8 Fix a vulnerability in chatMsg handler 2013-10-12 18:25:36 -05:00
calzoneman
babeb01ebe Add try-catch to playlist loading 2013-10-12 15:54:39 -05:00
Calvin Montgomery
af2ef83df8 Merge pull request #297 from nuclearace/patch-1
http://xkcd.com/859/
2013-10-11 23:38:03 -07:00
nuclearace
48eeefcb01 Update README.md 2013-10-12 02:37:11 -04:00
Calvin Montgomery
5deb07acec Update README.md 2013-10-12 01:34:54 -05:00
Calvin Montgomery
4f7240244f Merge pull request #296 from nuclearace/patch-1
Update README.md
2013-10-11 23:14:42 -07:00
nuclearace
a75364f724 Update README.md 2013-10-12 02:13:48 -04:00
Calvin Montgomery
141516bf36 Merge pull request #294 from calzoneman/svsingle
Server refactoring
2013-10-11 20:04:11 -07:00
calzoneman
2cb8ab3a98 Fix GET / 2013-10-11 18:20:52 -05:00
calzoneman
aad1c2995c Fixes 2013-10-11 18:15:44 -05:00
calzoneman
fa9656432d Remove rank.js 2013-10-11 16:31:54 -05:00
calzoneman
f2c996ebfa major refactoring 2013-10-11 16:31:40 -05:00
calzoneman
542461a533 Refactor out server init to index.js 2013-10-11 15:48:01 -05:00
calzoneman
128367bfc4 Do some server refactoring 2013-10-09 18:10:26 -05:00
calzoneman
a3153246ce Fix the only thing I see that could possibly still cause #285 2013-10-07 19:03:20 -05:00
calzoneman
f5cbdb2f24 Fix #291 2013-10-07 10:04:08 -05:00
calzoneman
718a70bc60 Fix clean; minor error message fixes 2013-10-07 00:10:16 -05:00
calzoneman
7d862cac60 Show links that failed for queueFail 2013-10-06 01:43:25 -05:00
calzoneman
ce197d3d8a Fix a bug with guestname capitalization 2013-10-05 20:42:15 -05:00
calzoneman
6309e42989 Tweak movement 2013-10-03 22:11:47 -05:00
calzoneman
89422c3c1d Fix #289 2013-10-03 22:07:44 -05:00
calzoneman
75ea06ed76 Add a small toggle for the MOTD 2013-10-02 22:26:28 -05:00
calzoneman
abc9c398f4 Fix media length limit 2013-10-02 09:37:13 -05:00
calzoneman
9d35636be2 Resolve merge conflict 2013-10-01 23:05:02 -05:00
calzoneman
a23a55f6a2 Update changelog and version # 2013-10-01 23:02:31 -05:00
calzoneman
1e2a142524 Fix 'error: true' 2013-10-01 14:26:32 -05:00
calzoneman
a1c72aaa8d Use asyncqueue clientside too 2013-10-01 13:35:29 -05:00
calzoneman
acb5136c15 Improve setCurrent 2013-10-01 13:25:05 -05:00
calzoneman
29a1f0b9d3 Throttle comma-separated lists properly 2013-10-01 13:14:44 -05:00
calzoneman
54dee9e25d Fix some error messages, intialize announcement to null 2013-09-30 21:59:04 -05:00
calzoneman
54016f6f48 Remove debug message; stack queueFail messages clientside 2013-09-30 21:54:01 -05:00
calzoneman
0c52c3f17d Fix /clean 2013-09-30 21:34:40 -05:00
calzoneman
3666b43f7a Use new asyncqueue system for moving and deleting 2013-09-30 10:05:09 -05:00
calzoneman
0eda0b8ed2 Fix comma separated queues 2013-09-30 09:51:38 -05:00
calzoneman
2fc8349daf Migrate youtube playlists and user playlists, comma lists sometimes break still 2013-09-29 22:32:37 -05:00
calzoneman
69ac0b39ad Check for videos where embedding is disabled 2013-09-29 21:32:04 -05:00
calzoneman
a371ca6440 Continue refactoring playlist 2013-09-29 19:53:27 -05:00
calzoneman
21bd270813 Improvements to playlist queue 2013-09-29 11:30:36 -05:00
calzoneman
3d3d21a3f9 Add asyncqueue class 2013-09-29 00:27:43 -05:00
calzoneman
8ba5743bc2 Improve link filter 2013-09-27 10:28:48 -05:00
calzoneman
ba66aadf66 Move interval stuff to config keys 2013-09-26 23:41:38 -05:00
calzoneman
6bf11a57b3 Generalize background tasks, rework aliases a bit 2013-09-26 23:36:00 -05:00
calzoneman
2f813c1d11 Remove mostly useless realtime connection stats 2013-09-26 23:10:00 -05:00
calzoneman
08a39c8857 Fix the bug where the player repeats the first few seconds at the end 2013-09-26 21:43:38 -05:00
calzoneman
6e18e25139 Code style cleanup on user.js 2013-09-26 13:29:36 -05:00
calzoneman
379522f2df A few minor cleanups 2013-09-26 13:18:37 -05:00
calzoneman
80215b5cdc nuclearace if you find one more thing wrong with search results I swear 2013-09-24 15:19:40 -05:00
calzoneman
87ee45165d Fix announcements for SSL sockets 2013-09-24 13:46:41 -05:00
calzoneman
b3f4fc2b52 Fix search result buttons issue 2013-09-24 13:19:41 -05:00
calzoneman
0addff6bab Don't add delete buttons to youtube search results
People kept clicking them resulting in database errors for unregistered channels.
2013-09-23 16:25:45 -05:00
calzoneman
851491e4ac Modify lead function for lead-in 2013-09-21 23:54:29 -05:00
calzoneman
6a2e7bb9e2 Quick buttons for modflair and adminflair 2013-09-21 02:22:51 -05:00
calzoneman
da29d35121 Add 'Nobody' permission setting 2013-09-21 00:30:47 -05:00
calzoneman
25d225ec97 Improve poll feedback 2013-09-21 00:25:00 -05:00
calzoneman
4517741455 A few minor changes to the /clean merge 2013-09-21 00:10:22 -05:00
Calvin Montgomery
04b9537afc Merge pull request #281 from unbibium/master
[MERGE] Add /clean and /cleantitle commands for moderators
2013-09-20 21:55:37 -07:00
Nick Bensema
1a4349945d Merge branch 'master' of https://github.com/unbibium/sync 2013-09-18 22:24:26 -07:00
Nick Bensema
cb94a2ac70 Removed help.html since it's not used anymore. 2013-09-18 22:23:23 -07:00
Nick Bensema
1e8b507750 Added online help for /clean and /cleantitle 2013-09-18 22:23:23 -07:00
Nick Bensema
8899b5f799 Added /cleantitle, and -i option to ignore case.
Fix #257, you should be able to clear username and title patterns
now with ease.  For best results, wait until they've wasted a lot
of time adding a lot of videos before you wipe them.
2013-09-18 22:23:23 -07:00
Nick Bensema
bf95be812c made slightly more efficient in response to comments 2013-09-18 22:23:23 -07:00
Nick Bensema
c6c6e8e55b Added /clean command to clear a user's additions from a channel
Any user with playlistdelete permission can now use the /clean
command to wipe out a user's additions to the playlist.  This
can be helpful for taking down abusive items en masse without
resetting the entire playlist.

The code should probably be generalized, somehow, to handle
title patterns or additions within a certain timeframe.
2013-09-18 22:23:23 -07:00
Calvin Montgomery
326a839cc0 Merge pull request #280 from calzoneman/closurefix
Closurefix
2013-09-18 16:30:38 -07:00
calzoneman
1d9845f6d5 Change channel checks in user.js 2013-09-18 18:27:42 -05:00
calzoneman
39fe452e96 Add extra checks to channel.js for deadness 2013-09-18 18:16:12 -05:00
calzoneman
d5c5de41e1 Fix what I assume was a race condition 2013-09-17 22:26:44 -05:00
calzoneman
6aae146f8b Fix ustream for channels with /channel/foo syntax; ustream wtf are you doing 2013-09-17 13:19:33 -05:00
calzoneman
3f65a69f0e Fix reset password 2013-09-13 10:24:54 -05:00
calzoneman
2b0e6a3dbf Fix index page 2013-09-12 18:42:22 -05:00
calzoneman
5497a868a3 Fix an error 2013-09-12 16:26:45 -05:00
calzoneman
408f6626bb Fix behavior of chat filter XSS 2013-09-12 13:03:04 -05:00
calzoneman
9d445b8ffd Fix up voteskip a bit 2013-09-11 22:16:56 -05:00
calzoneman
7e9673425d Add support for hidden polls 2013-09-11 20:22:00 -05:00
calzoneman
020ceecd40 Add SSL user options so the page can be plain HTTP 2013-09-10 22:43:43 -05:00
calzoneman
beae68d1c2 Fix everything except justin.tv, twitch.tv 2013-09-10 17:18:02 -05:00
calzoneman
aadba26891 Fix disabling ssl causing errors 2013-09-10 16:45:43 -05:00
calzoneman
a085ed83f2 Fix JWPlayer for SSL 2013-09-10 16:34:39 -05:00
calzoneman
2a30d30d0a Trust X-Forwarded-For from 127.0.0.1 2013-09-10 16:11:35 -05:00
calzoneman
83ea83429e Add passphrase support to SSL 2013-09-10 14:10:30 -05:00
calzoneman
4ba8e88e27 Merge ssl into master 2013-09-10 13:51:47 -05:00
calzoneman
efcae43ae8 Fix issues with regard to unloading channels 2013-09-09 22:11:24 -05:00
calzoneman
4ec1d04247 Add SSL support 2013-09-09 17:16:41 -05:00
calzoneman
f7e968a13c Alter channel unload behavior, add additional checks
- Should prevent "write after end" errors caused by unloading a channel before it finishes loading
- Might prevent strange cases of playlists gone wild
2013-09-08 17:43:30 -05:00
calzoneman
b3ea7069a8 Hopefully fix the duplicate login kick issue for retarded browsers 2013-09-07 23:40:25 -05:00
calzoneman
4c34625d48 Add an extra check and log message 2013-09-07 15:44:57 -05:00
calzoneman
bca7199ec1 Add io-host option 2013-09-06 16:30:20 -05:00
calzoneman
6aecb32c89 Fix #277 2013-09-06 15:53:23 -05:00
calzoneman
c2cd04f760 Fix #276 2013-09-05 22:54:30 -05:00
Calvin Montgomery
6d958b4b31 Remove irrelevant database test from ages ago 2013-09-05 16:44:43 -05:00
Calvin Montgomery
b3fb8c557c Delete .logger.js.swp 2013-09-05 13:49:58 -05:00
calzoneman
7840fa35e8 Move server files to lib/ to clean up root directory 2013-09-05 13:48:05 -05:00
Nick Bensema
1ddba6d39b Removed help.html since it's not used anymore. 2013-09-04 20:46:57 -07:00
calzoneman
7b54b2fcc0 Add a changelog file for detailed explanation of changes 2013-09-04 22:46:50 -05:00
Nick Bensema
cbaa1f04e9 Added online help for /clean and /cleantitle 2013-09-04 19:39:38 -07:00
Nick Bensema
507a97b4db Added /cleantitle, and -i option to ignore case.
Fix #257, you should be able to clear username and title patterns
now with ease.  For best results, wait until they've wasted a lot
of time adding a lot of videos before you wipe them.
2013-09-04 19:27:48 -07:00
calzoneman
762b4fa6af Address #270 2013-09-04 17:47:24 -05:00
Nick Bensema
48de471064 made slightly more efficient in response to comments 2013-09-03 21:28:12 -07:00
calzoneman
5312911c15 Do some additional validation on IDs to prevent database errors 2013-09-03 22:23:05 -05:00
calzoneman
234456f2f4 A couple tweaks to alternate dark 2013-09-03 15:10:11 -05:00
Calvin Montgomery
4a1aea875f Fix typo 2013-09-03 12:49:02 -05:00
Nick Bensema
c905280125 Added /clean command to clear a user's additions from a channel
Any user with playlistdelete permission can now use the /clean
command to wipe out a user's additions to the playlist.  This
can be helpful for taking down abusive items en masse without
resetting the entire playlist.

The code should probably be generalized, somehow, to handle
title patterns or additions within a certain timeframe.
2013-09-02 23:36:19 -07:00
calzoneman
e6c4097250 Fix footer color 2013-09-02 14:56:17 -05:00
calzoneman
a5a83c4121 Add alternative dark theme 2013-09-02 14:53:59 -05:00
calzoneman
3ad925ad84 Merge branch 'master' of github.com:calzoneman/sync 2013-09-02 12:40:15 -05:00
calzoneman
91fd44808e Prevent borders on <iframe> in custom media 2013-09-02 12:40:06 -05:00
Calvin Montgomery
0a9caa7fee Merge pull request #275 from nuclearace/patch-2
Scroll to current video after adding buttons
2013-09-02 10:34:27 -07:00
nuclearace
ce4c5fc67f Scroll to current video after adding buttons 2013-09-02 13:09:14 -04:00
Calvin Montgomery
0d3d3b4d7d Merge pull request #274 from calzoneman/optionsmenu
Redesign user settings menu
2013-08-31 10:41:44 -07:00
calzoneman
3729736bc9 Need to add the HTML template 2013-08-31 12:38:20 -05:00
calzoneman
3b5f6c64ef Finish refactoring options menu 2013-08-31 12:37:37 -05:00
calzoneman
7ae76bbe31 Finish desining the new options menu 2013-08-30 22:37:00 -05:00
calzoneman
dfb5bd011b Start refactoring the options menu 2013-08-30 22:12:28 -05:00
calzoneman
7ae382295f Remove debug timeout 2013-08-30 18:43:09 -05:00
calzoneman
f90965c105 Fix login race condition which caused rank loss 2013-08-30 18:42:00 -05:00
calzoneman
bd348132e6 By default push userlist to the right on synchtube layout 2013-08-30 15:49:30 -05:00
calzoneman
ac8754299c Make the light theme a bit less blinding 2013-08-30 15:47:52 -05:00
calzoneman
b603e392e6 Add an extra check that might prevent stuck profile hovers 2013-08-28 23:30:54 -05:00
Calvin Montgomery
02887958cb Tweak rate limiting on the playlist 2013-08-28 19:25:53 -05:00
Calvin Montgomery
202c2ec467 Merge pull request #273 from nuclearace/patch-1
Correct a typo in ui.js
2013-08-28 16:51:26 -07:00
nuclearace
3b734e55a2 typo 2013-08-28 19:49:07 -04:00
Calvin Montgomery
613acd2055 Implement #251 (sound may change) 2013-08-28 16:03:31 -05:00
calzoneman
27ef20a76a Fix a slight bug regarding playlist meta 2013-08-28 10:38:24 -05:00
calzoneman
4884d8d691 Fix #272 2013-08-28 10:32:35 -05:00
calzoneman
eee17f9daa For convenience, queue next bumps if the item is already on the playlist 2013-08-26 16:37:09 -05:00
calzoneman
c8ce8b530a Add a few limits 2013-08-26 10:36:47 -05:00
calzoneman
0491ea11da Merge branch 'master' of github.com:calzoneman/sync 2013-08-25 22:06:57 -05:00
calzoneman
1230e8493d Maybe this will solve the mystery of the floating clocks? Time will tell 2013-08-25 22:06:32 -05:00
Calvin Montgomery
dd0410eac7 Fix video lengths not showing in library 2013-08-23 21:06:29 -05:00
Calvin Montgomery
0e87fc6907 Fix #269 2013-08-23 18:41:48 -05:00
calzoneman
36445bed6e Allow renaming Custom Embed items (#255) 2013-08-22 17:33:03 -05:00
calzoneman
1294a7bd50 Fix banning guest names, add ban message to chat 2013-08-22 15:14:17 -05:00
calzoneman
6f0e1e96a1 A few minor fixes, also #261 2013-08-21 23:10:55 -05:00
calzoneman
5f70e9dc80 Fix database typo 2013-08-21 23:05:20 -05:00
calzoneman
22ccde12e7 Merge branch 'master' of github.com:calzoneman/sync 2013-08-21 19:22:14 -05:00
calzoneman
4841e7bc1c Add an extra check to prevent sticking usernames clientside 2013-08-21 19:20:26 -05:00
Calvin Montgomery
b45dfac99b Update README.md 2013-08-21 18:56:52 -05:00
calzoneman
3abc7ca0c6 Don't search library or cache media if not registered 2013-08-21 16:25:01 -05:00
calzoneman
6d47a94fe9 Fix db key issue for bans - run node update.js 2013-08-21-banfix 2013-08-21 16:09:28 -05:00
calzoneman
d5ebf1ad4e Fix database.js 2013-08-20 19:56:03 -05:00
calzoneman
98853822e0 Slight fix to update.js 2013-08-20 19:53:30 -05:00
calzoneman
ffddc434f2 Fix UTF8 issues - run 'node update.js 2013-08-20-utf8fix' to correct your database 2013-08-20 19:50:49 -05:00
calzoneman
e830ba2d41 Don't query the library table for live media 2013-08-20 14:20:21 -05:00
Calvin Montgomery
e415e1e1d6 Merge pull request #264 from calzoneman/dev
Experiment with realtime stats of connection load
2013-08-20 08:50:25 -07:00
calzoneman
e748d79349 Experiment with realtime stats of connection load 2013-08-19 23:53:33 -05:00
Calvin Montgomery
89f4da20c5 Merge pull request #263 from calzoneman/dev
v2.4 - run npm update
2013-08-19 11:11:55 -07:00
calzoneman
779bdb4067 Don't show socket error message when the server dies 2013-08-19 11:10:52 -05:00
calzoneman
49dc501a8e Fix typo 2013-08-19 11:08:55 -05:00
calzoneman
8d9b056af5 Add the global flag to IP filtering on channel log 2013-08-19 00:40:16 -05:00
calzoneman
1acf27d867 dev is now in final testing stages 2013-08-19 00:27:30 -05:00
calzoneman
8b3ae3b546 Merge dev into dbrefactor 2013-08-19 00:24:48 -05:00
calzoneman
d62931098d Merge branch 'master' into dev 2013-08-19 00:23:13 -05:00
calzoneman
d100936b65 A couple fixes 2013-08-18 19:31:34 -05:00
calzoneman
e3ef9e7896 Add a special notice 2013-08-18 18:35:49 -05:00
calzoneman
1f7a53a90b Fix a crash condition 2013-08-18 18:03:49 -05:00
calzoneman
bab2b887f4 Fix channel.js bug, add defer to APIs for faster page loads 2013-08-18 18:01:57 -05:00
calzoneman
b3526b5ee2 Revisit some banning issues 2013-08-18 17:58:16 -05:00
calzoneman
08a46f5e00 Fixes 2013-08-18 14:21:42 -05:00
calzoneman
c9b5254f24 Fix rank change for person in channel 2013-08-18 13:35:57 -05:00
calzoneman
aa13cc95ec Add a few checks 2013-08-18 13:16:46 -05:00
calzoneman
bfc420336a Fix global ban typo 2013-08-18 13:09:51 -05:00
calzoneman
806cbb3336 Fix channel rank change 2013-08-18 13:08:31 -05:00
calzoneman
7254512e7d Fix a couple bugs 2013-08-18 13:05:12 -05:00
calzoneman
dfcdee7637 Fix user.js crash 2013-08-18 12:49:54 -05:00
calzoneman
e4d0d21667 Don't display session expired 2013-08-18 12:48:49 -05:00
calzoneman
4253d3c84b Fix the other POST api calls besides just /api/login 2013-08-18 12:24:32 -05:00
calzoneman
2f4621dab9 Start fixing things 2013-08-18 12:21:34 -05:00
calzoneman
42e89dc557 Hack around cross-domain POSTing 2013-08-17 22:41:54 -05:00
calzoneman
73dbc4ffab Minor fix for API 2013-08-17 21:00:50 -05:00
calzoneman
9f52ca7eec Forgot to add to commit 2013-08-17 20:22:34 -05:00
calzoneman
26e654cf5a Oh yeah, use the actual logger 2013-08-17 18:52:13 -05:00
calzoneman
492a50fb96 Well the process starts, that's a start 2013-08-17 18:51:10 -05:00
calzoneman
f4b32ad3ad Make config saving synchronous 2013-08-17 18:45:21 -05:00
calzoneman
b686deb16f Refactoring 2013-08-17 18:44:48 -05:00
calzoneman
65d7a8a455 Fix user.js for new auth 2013-08-17 15:54:23 -05:00
calzoneman
ebd55b4846 Remove unnecessary import 2013-08-17 15:47:24 -05:00
calzoneman
4204ea8ddf Finish channel.js 2013-08-17 15:47:11 -05:00
calzoneman
e155a30a17 Do a bit more work 2013-08-17 12:37:35 -05:00
calzoneman
b7c02334ed Start on channel.js 2013-08-17 12:30:52 -05:00
calzoneman
05dc2a3119 Add a domain handler to prevent service outages from requiring a server restart 2013-08-16 21:35:21 -05:00
calzoneman
448d50e796 Amend last commit 2013-08-16 13:26:31 -05:00
calzoneman
063b179ab3 Fix ActionLog calls for api 2013-08-16 13:25:36 -05:00
calzoneman
3f26fc80e0 Finish fixing api.js 2013-08-16 13:19:00 -05:00
calzoneman
f46169fbe3 Start updating auth dependencies 2013-08-16 11:01:31 -05:00
calzoneman
f523649f54 Finish refactoring api 2013-08-16 10:37:26 -05:00
calzoneman
a20df07515 Start deprecating auth.js 2013-08-15 22:18:10 -05:00
calzoneman
5969558320 Start fucking api.js 2013-08-15 19:51:03 -05:00
calzoneman
e7b22997c7 Update acp.js to be compatible with new actionlog 2013-08-15 19:39:05 -05:00
calzoneman
b342be50a3 Refactor actionlog queries 2013-08-15 17:44:22 -05:00
calzoneman
c4d5b1cd1d finish refactoring acp (kind of -- depends on auth 2013-08-15 14:43:35 -05:00
calzoneman
6d84222871 Start refactoring acp.js database calls 2013-08-15 13:53:58 -05:00
calzoneman
d883445ed4 Finish refactoring existing functions from database.js 2013-08-15 13:39:32 -05:00
calzoneman
1b9c707bdf Refactor some password functions and user playlists 2013-08-15 12:18:13 -05:00
calzoneman
8776ae15fc Solve some database reconnection issues 2013-08-15 09:47:52 -05:00
calzoneman
98b6273dc4 Implement #259 2013-08-14 23:26:50 -05:00
calzoneman
be315e9e23 Remove syntax error 2013-08-14 22:52:21 -05:00
calzoneman
8a2dfa004d Continue work on refactoring, add utilities module 2013-08-14 22:51:59 -05:00
Calvin Montgomery
26a46fc1d9 Fix text wrap on profile-box, change loaded channels to public channels 2013-08-13 22:17:58 -04:00
calzoneman
0c1e7642d9 Add some conditions that should prevent logging errors 2013-08-13 14:51:39 -04:00
calzoneman
8a10f82089 Fix error message 2013-08-13 11:33:32 -04:00
calzoneman
40020684b8 Restrict moderators too 2013-08-13 11:23:39 -04:00
calzoneman
40f3e875a8 2.3.3 - run update.js 2013-08-13 11:18:30 -04:00
calzoneman
f910f437bb Disallow non-moderators from adding a video that exists 10 times already 2013-08-13 11:18:06 -04:00
calzoneman
77a2d9b0e2 Limit video titles to 100 characters - run update.js to patch your database 2013-08-13 11:07:02 -04:00
calzoneman
817d5b85e0 Limit dump size to 1MB 2013-08-13 10:46:18 -04:00
Calvin Montgomery
9675edadf5 Continue migrating/refactoring 2013-08-12 23:23:10 -04:00
calzoneman
a112700600 Work on channel db stuff 2013-08-12 14:23:32 -04:00
calzoneman
8822fc5206 Continue work on db 2013-08-12 10:58:21 -04:00
calzoneman
44d5f42a36 Start switching to node-mysql 2013-08-12 10:34:57 -04:00
Calvin Montgomery
0ceb362f0b Remove faulty requestPlaylist 2013-08-12 00:27:30 -04:00
Calvin Montgomery
01eeab0711 Clear queued playlist actions on new playlist 2013-08-12 00:24:48 -04:00
Calvin Montgomery
66f66505af Merge branch 'master' into apirefactor 2013-08-12 00:09:50 -04:00
Calvin Montgomery
0d31d6eea2 Fix stupid typos 2013-08-12 00:09:43 -04:00
Calvin Montgomery
03e27a7720 Various fixes to the API 2013-08-11 23:36:42 -04:00
Calvin Montgomery
4aa0e7a4ef Start updating to new API 2013-08-11 23:10:55 -04:00
Calvin Montgomery
0bf80a375d Refactor log reading 2013-08-11 22:32:02 -04:00
Calvin Montgomery
b06d8ff09f Refactor register, my channels 2013-08-11 22:20:09 -04:00
calzoneman
25a877dc3c Refactor password recover, email set, profile get/set 2013-08-11 18:55:53 -04:00
calzoneman
c4588fab49 Refactor password change and reset 2013-08-11 18:23:20 -04:00
calzoneman
9c22fbc462 Add try-catch to urlRetrieve to prevent uncaught HTTPS exceptions 2013-08-11 17:44:48 -04:00
calzoneman
dba93ca3b6 Fix #254 2013-08-11 17:39:10 -04:00
calzoneman
dbad8cac2f Merge branch 'master' of github.com:calzoneman/sync 2013-08-09 16:32:38 -04:00
calzoneman
293576a3af Fix #244 again. I want to get off autotemp's wild ride 2013-08-09 16:32:05 -04:00
calzoneman
637eadea23 Fix #249. I don't even want to calculate the odds of someone saying a message that contains /afk not at the beginning while being afk. 2013-08-09 16:28:25 -04:00
Calvin Montgomery
8f190403f8 Fix leader bug 2013-08-08 23:23:19 -04:00
calzoneman
539d728df4 Fix weird case of playlist skipping when removing leader 2013-08-08 20:42:46 -04:00
calzoneman
e53a0eee4c Increment version number 2013-08-08 18:28:24 -04:00
calzoneman
4f6d72d6d5 Resolve merge conflict 2013-08-08 18:28:10 -04:00
calzoneman
07a3e3cc21 A few minor changes
- Fix paginator always showing 5 buttons even if there are less pages
- Clear AFK timer when clicking voteskip
- Change the modestbranding youtube param because people wanted the like/dislike buttons
2013-08-08 18:25:56 -04:00
calzoneman
9256928d82 Handle user impersonation using bold filter differently 2013-08-08 10:57:46 -04:00
calzoneman
c7501bfd50 Add different userlist sort options 2013-08-08 10:54:07 -04:00
calzoneman
425aa75165 Show popup with breakdown of usercount on hover 2013-08-08 10:39:58 -04:00
Calvin Montgomery
d266175d5b Start working on API refactor 2013-08-07 23:44:41 -04:00
calzoneman
9c83a4dd3e Fix #248, fix channel caching temp media 2013-08-07 21:05:30 -04:00
calzoneman
ac9d60eee7 Fix browser compat issue 2013-08-07 17:55:39 -04:00
368 changed files with 176121 additions and 38627 deletions

45
.eslintrc.js Normal file
View file

@ -0,0 +1,45 @@
/* ESLint Config */
module.exports = {
env: {
'es2017': true,
// others envs defined by cascading .eslintrc files
},
extends: 'eslint:recommended',
parser: '@babel/eslint-parser',
parserOptions: {
'sourceType': 'module',
},
rules: {
'brace-style': ['error','1tbs',{ 'allowSingleLine': true }],
'indent': [
'off', // temporary... a lot of stuff needs to be reformatted | 2020-08-21: I guess it's not so temporary...
4,
{ 'SwitchCase': 1 }
],
'linebreak-style': ['error','unix'],
'no-control-regex': ['off'],
'no-prototype-builtins': ['off'], // should consider cleaning up the code and turning this back on at some point
'no-trailing-spaces': ['error'],
'no-unused-vars': [
'error', {
'argsIgnorePattern': '^_',
'varsIgnorePattern': '^_|^Promise$'
}
],
'semi': ['error','always'],
'quotes': ['off'] // Old code uses double quotes, new code uses single / template
},
ignorePatterns: [
// These are not ours
'www/js/dash.all.min.js',
'www/js/jquery-1.12.4.min.js',
'www/js/jquery-ui.js',
'www/js/peertube.js',
'www/js/playerjs-0.0.12.js',
'www/js/sc.js',
'www/js/video.js',
'www/js/videojs-contrib-hls.min.js',
'www/js/videojs-dash.js',
'www/js/videojs-resolution-switcher.js',
],
}

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
*.swp
cfg.json
config.yaml
chandump
chanlogs
*.log
node_modules
*.crt
*.cert
*.key
torlist
www/cache
google-drive-subtitles
lib/
integration-test-config.json
conf/*.toml
www/js/cytube-google-drive.user.js
www/js/cytube-google-drive.meta.js
www/js/player.js
tor-exit-list.json
*.patch
examples/demo-bot/.env
mysql/
peertube-hosts.json

14
.travis.yml Normal file
View file

@ -0,0 +1,14 @@
language: node_js
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- gcc-9
- g++-9
env:
- CXX="g++-9"
node_js:
- "15"
- "14"
- "12"

18
Dockerfile Normal file
View file

@ -0,0 +1,18 @@
FROM node:20
RUN apt update && apt install -y build-essential git wget
COPY ./ /app
WORKDIR /app
RUN rm -rf node_modules lib package-lock.json
RUN npm cache clean --force
RUN npm install
RUN /bin/sh /app/postinstall.sh
RUN npm run build-server
RUN npm rebuild
RUN wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp
RUN chmod a+rx /usr/local/bin/yt-dlp
CMD ["node", "index.js"]

47
ISSUE_TEMPLATE.md Normal file
View file

@ -0,0 +1,47 @@
Please fill out the templates below to the best of your ability, based on whether your problem is with using the website or with running your own server.
## Website Problem ##
**Please confirm whether you've tried the following debugging steps:**
- [ ] Clearing cache and refreshing the page (On Firefox, press Ctrl+F5. On Chrome, press F12, then right click the refresh button and click "Empty Cache and Hard Reload")
- [ ] Disabling all browser extensions
- [ ] Using a clean channel with no customizations
### Description of the Problem ###
- What triggers the problem?
- What happens?
- What do you expect to happen instead?
### System Information ###
- **Operating System (Windows / Mac / Linux / Android / iOS):**
- **Web Browser (Firefox / Chrome / Other):**
- **Error Messages Displayed:**
- **Screenshot of JavaScript Console:**
_On Firefox, press `Ctrl+Shift+K` to open the JavaScript console. On Chrome, press `Ctrl+Shift+J`._
## Server Problem ##
_If your issue is related to using the website and not about running a server, you can remove this section._
**Please confirm whether you've tried the following debugging steps:**
- [ ] Run `npm run build-server` to regenerate `lib/` from `src/`
- [ ] Run `rm -rf node_modules && npm install` to get a fresh install of dependencies
- [ ] Restarted the server
### Description of the Problem ###
- What triggers the problem?
- What happens?
- What do you expect to happen instead?
### System Information ###
- **Operating System:**
- **Node Version:** _(run `node -v`)_
- **CyTube Version:** _(displayed at startup)_
- **Error Messages Displayed:**

View file

@ -1,6 +1,6 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Copyright (c) 2013-2022 Calvin Montgomery and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

501
NEWS.md Normal file
View file

@ -0,0 +1,501 @@
2022-09-21
==========
**Upgrade intervention required**
This release adds a feature to ban channels, replacing the earlier (hastily
added) configuration-based `channel-blacklist`. If you have any entries in
`channel-blacklist` in your `config.yaml`, you will need to migrate them to the
new bans table by using a command after upgrading (the ACP web interface hasn't
been updated for this feature):
./bin/admin.js ban-channel <channel-name> <external-reason> <internal-reason>
The external reason will be displayed when users attempt to join the banned
channel, while the internal reason is only displayed when using the
`show-channel-ban` command.
You can later use `unban-channel` to remove a ban. The owner of the banned
channel can still delete it, but the banned state will persist, so the channel
cannot be re-registered later.
2022-08-28
==========
This release integrates Xaekai's added support for Bandcamp, BitChute, Odysee,
and Nicovideo playback support into the main repository. The updated support
for custom fonts and audio tracks in custom media manifests is also included,
but does not work out of the box -- it requires a separate channel script; this
may be addressed in the future.
2021-08-14
==========
CyTube has been upgraded to socket.io v4 (from v2).
**Breaking change:** Newer versions of socket.io require CORS to validate the
origin initiating the socket connection. CyTube allows the origins specified in
the `io.domain` and `https.domain` configuration keys by default, which should
work for many use cases, however, if you host your website on a different domain
than the socket connection, you will need to configure the allowed origins (see
config.template.yaml under `io.cors`).
CyTube enables the `allowEIO3` configuration in socket.io by default, which
means that existing clients and bots using socket.io-client v2 should continue
to work.
2021-08-12
==========
The legacy metrics recorder (`counters.log` file) has been removed. For over 4
years now, CyTube has integrated with [Prometheus](https://prometheus.io/),
which provides a superior way to monitor the application. Copy
`conf/example/prometheus.toml` to `conf/prometheus.toml` and edit it to
configure CyTube's Prometheus support.
2021-08-12
==========
Due to changes in Soundcloud's authorization scheme, support has been dropped
from core due to requiring each server owner to register an API key (which is
currently impossible as they have not accepted new API key registrations for
*years*).
If you happen to already have an API key registered, or if Soundcloud reopens
registration at some point in the future, feel free to reach out to me for
patches to reintroduce support for it.
2020-08-21
==========
Some of CyTube's dependencies depends on features in newer versions of node.js.
Accordingly, node 10 is no longer supported. Administrators are recommended to
use node 12 (the active LTS), or node 14 (the current version).
2020-06-22
==========
Twitch has [updated their embed
player](https://discuss.dev.twitch.tv/t/twitch-embedded-player-migration-timeline-update/25588),
which adds new requirements for embedding Twitch:
1. The origin website must be served over HTTPS
2. The origin website must be served over the default port (i.e., the hostname
cannot include a port; https://example.com:8443 won't work)
Additionally, third-party cookies must be enabled for whatever internal
subdomains Twitch is using.
CyTube now sets the parameters expected by Twitch, and displays an error message
if it detects (1) or (2) above are not met.
2020-02-15
==========
Old versions of CyTube defaulted to storing channel state in flatfiles located
in the `chandump` directory. The default was changed a while ago, and the
flatfile storage mechanism has now been removed.
Admins who have not already migrated their installation to the "database"
channel storage type can do so by following these instructions:
1. Run `git checkout e3a9915b454b32e49d3871c94c839899f809520a` to temporarily
switch to temporarily revert to the previous version of the code that
supports the "file" channel storage type
2. Run `npm run build-server` to build the old version
3. Run `node lib/channel-storage/migrator.js |& tee migration.log` to migrate
channel state from files to the database
4. Inspect the output of the migration tool for errors
5. Set `channel-storage`/`type` to `"database"` in `config.yaml` and start the
server. Load a channel to verify the migration worked as expected
6. Upgrade back to the latest version with `git checkout 3.0` and `npm run
build-server`
7. Remove the `channel-storage` block from `config.yaml` and remove the
`chandump` directory since it is no longer needed (you may wish to archive
it somewhere in case you later discover the migration didn't work as
expected).
If you encounter any errors during the process, please file an issue on GitHub
and attach the output of the migration tool (which if you use the above commands
will be written to `migration.log`).
2019-12-01
==========
In accordance with node v8 LTS becoming end-of-life on 2019-12-31, CyTube no
longer supports v8.
Please upgrade to v10 or v12 (active LTS); refer to
https://nodejs.org/en/about/releases/ for the node.js support timelines.
2018-12-07
==========
Users can now self-service request their account to be deleted, and it will be
automatically purged after 7 days. In order to send a notification email to
the user about the request, copy the [email
configuration](https://github.com/calzoneman/sync/blob/3.0/conf/example/email.toml#L43)
to `conf/email.toml` (the same file used for password reset emails).
2018-10-21
==========
The `sanitize-html` dependency has made a change that results in `"` no longer
being replaced by `&quot;` when not inside an HTML attribute value. This
potentially breaks any chat filters matching quotes as `&quot;` (on my
particular instance, this seems to be quite rare). These filters will need to
be updated in order to continue matching quotes.
2018-08-27
==========
Support for node.js 6.x has been dropped, in order to bump the babel preset to
generate more efficient code (8.x supports async-await and other ES6+ features
natively and is the current node.js LTS).
If you are unable to upgrade to node.js 8.x, you can revert the changes to
package.json in this commit, however, be warned that I no longer test on 6.x.
2018-06-03
==========
## Dependency upgrades
In order to support node.js 10, the `bcrypt` dependency has been upgraded to
version 2. `bcrypt` version 2 defaults to the `$2b$` algorithm, whereas version
1 defaults to the `$2a$` algorithm. Existing password hashes will continue to
be readable, however hashes created with version 2 will not be readable by
version 1. See https://github.com/kelektiv/node.bcrypt.js for details.
In addition, the optional dependency on `v8-profiler` has been removed, since
this is not compatible with newer versions of v8.
## Supported node.js versions
In accordance with the node.js release schedule, node.js 4.x, 5.x, 7.x, and 9.x
are end-of-life and are no longer maintained upstream. Accordingly, these
versions are no longer supported by CyTube.
Please upgrade to 8.x (LTS) or 10.x (current). 6.x is still supported, but is
in the "maintenance" phase upstream, and should be phased out.
2018-01-07
==========
**Build changes:** When the `babel` dependency was first added to transpile ES6
code to ES5, an interactive prompt was added to the `postinstall` script before
transpilation, in case the user had made local modifications to the files in
`lib` which previously would have been detected as a git conflict when pulling.
It has now been sufficiently long that this is no longer needed, so I've removed
it. As always, users wishing to make local modifications (or forks) should edit
the code in `src/` and run `npm run build-server` to regenerate `lib/`.
This commit also removes the bundled `www/js/player.js` file in favor of having
`postinstall` generate it from the sources in `player/`.
2017-12-24
==========
As of December 2017, Vid.me is no longer in service. Accordingly, Vid.me
support in CyTube has been deprecated.
2017-11-27
==========
The Google Drive userscript has been updated once again. Violentmonkey is
now explicitly supported. Google login redirects are caught and handled.
See directly below on how to regenerate the user script again.
2017-11-15
==========
The Google Drive userscript has been updated due to breaking changes in
Greasemonkey 4.0. Remember to generate the script by running:
$ npm run generate-userscript "Your Site Name" http://your-site.example.com/r/*
2017-11-05
==========
The latest commit introduces a referrer check in the account page handlers.
This is added as a short-term mitigation for a recent report that account
management functions (such as deleting channels) can be executed without the
user's consent if placed in channel JS.
Longer term options are being considered, such as moving account management to a
separate subdomain to take advantage of cross-origin checks in browsers, and
requiring the user to re-enter their password to demonstrate intent. As always,
I recommend admins take extreme caution when accepting channel JS.
2017-09-26
==========
**Breaking change:** the `nodemailer` dependency has been upgraded to version
4.x. I also took this opportunity to make some modifications to the email
configuration and move it out of `config.yaml` to `conf/email.toml`.
To upgrade:
* Run `npm upgrade` (or `rm -rf node_modules; npm install`)
* Copy `conf/example/email.toml` to `conf/email.toml`
* Edit `conf/email.toml` to your liking
* Remove the `mail:` block from `config.yaml`
This feature only supports sending via SMTP for now. If there is demand for
other transports, feel free to open an issue or submit a pull request.
2017-09-19
==========
The `/useragreement` default page has been removed. Server administrators can
substitute their own terms of service page by editing `templates/footer.pug`
2017-09-19
==========
This commit removes an old kludge that redirected users to HTTPS (when enabled)
specifically for the account authorization pages (e.g., `/login`). The code for
doing this was to work around limitations that no longer exist, and does not
represent current security best practices.
The recommended solution to ensure that users are logged in securely (assuming
you've configured support for HTTPS) is to use
[Strict-Transport-Security](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)
to direct browsers to access the HTTPS version of the website at all times. You
can enable this by configuring a reverse proxy (e.g. nginx) in front of CyTube
to intercept HTTP traffic and redirect it to HTTPS, and add the
`Strict-Transport-Security` header when returning the response from CyTube.
2017-07-22
==========
Support for the old version of Vimeo's OAuth API (the `vimeo-oauth`
configuration block) has been dropped. It's unlikely anyone was using this,
since you haven't been able to register new API keys for it in years (it was
superseded by a newer OAuth API, which CyTube does not support), and in fact I
lost my credentials for this API and no longer have a way to test it.
Vimeo videos can still be added -- the metadata will be queried from the
anonymous API which has been the default since the beginning.
2017-07-17
==========
The `stats` database table and associated ACP subpage have been removed in favor
of integration with [Prometheus](https://prometheus.io/). You can enable
Prometheus reporting by copying `conf/example/prometheus.toml` to
`conf/prometheus.toml` and editing it to your liking. I recommend integrating
Prometheus with [Grafana](https://grafana.com/) for dashboarding needs.
The particular metrics that were saved in the `stats` table are reported by the
following Prometheus metrics:
* Channel count: `cytube_channels_num_active` gauge.
* User count: `cytube_sockets_num_connected` gauge (labeled by socket.io
transport).
* CPU/Memory: default metrics emitted by the
[`prom-client`](https://github.com/siimon/prom-client) module.
More Prometheus metrics will be added in the future to make CyTube easier to
monitor :)
2017-07-15
==========
The latest commit upgrades `socket.io` to version 2.0, a major version change
from 1.4. This release improves performance by switching to `uws` for the
websocket transport, and fixes several bugs; you can read about it
[here](https://github.com/socketio/socket.io/releases/tag/2.0.0).
For browser clients, the upgrade should basically just work with no
intervention. For node.js clients, all that is needed is to upgrade
`socket.io-client` to 2.0. For other clients, work required may vary depending
on whether the implementation has compatibility problems with 2.0.
2017-06-20
==========
The latest commit drops support for node.js versions below 6 (the [current
LTS](https://github.com/nodejs/LTS#lts-schedule1)). This is to allow the babel
preset to avoid generating inefficient code to polyfill ES2015+ features that
are now implemented in the node.js core.
New versions of node.js can be downloaded from the [node.js
website](https://nodejs.org/en/download/), if they are not already available in
your distribution's package manager.
2017-03-20
==========
Polls are now more strictly validated, including the number of options. The
default limit is 50 options, which you can configure via `poll.max-options`.
2017-03-11
==========
Commit f8183bea1b37154d79db741ac2845adf282e7514 modifes the schema of the
`users` table to include a new column (`name_dedupe`) which has a `UNIQUE`
constraint. This column is populated with a modified version of the user's name
to prevent the registration of usernames which are bitwise distinct but visually
similar. 'l', 'L', and '1' are all mapped to '1'; 'o', 'O', and '0' are all
mapped to '0'; '\_' and '-' are mapped to '\_'. On first startup after
upgrading, the new column will be added and populated.
This replaces the earlier solution which was put in place to mitigate PR#489 but
was overly-restrictive since it wildcarded these characters against *any*
character, not just characters in the same group.
2017-03-03
==========
The dependency on `sanitize-html`, which previously pointed to a fork, has now
been switched back to the upstream module. XSS filtering has been turned off
for the chat filter replacement itself (since this provides no additional
security), and is now only run on the final chat message after filtering.
Certain chat filters and MOTDs which relied on syntactically incorrect HTML,
such as unclosed tags, may have different behavior now, since `sanitize-html`
fixes these.
2016-11-02
==========
After upgrading the dependency on `yamljs`, you may see this error if you didn't
notice and correct a typo in the config.yaml template:
Error loading config file config.yaml:
{ [Error: Unexpected characters near ",".]
message: 'Unexpected characters near ",".',
parsedLine: 88,
snippet: 'title: \'CyTube\',' }
The fix is to edit config.yaml and remove the trailing comma for the `title:`
property under `html-template`. If there are other syntax errors that the old
version didn't detect, you will need to correct those as well.
Longer term, I am looking to move away from using `yamljs` to parse
configuration because it's a little buggy and the current configuration system
is confusing.
2016-10-20
==========
Google Drive changed the URL schema for retrieving video metadata, which broke
CyTube's Google Drive support, even with the userscript. I have updated the
userscript source with the new URL, so server administrators will have to
regenerate the userscript for their site and users will be prompted to install
the newer version.
Additionally, fixing Drive lookups required an update to the `mediaquery`
module, so you will have to do an `npm install` to pull that fix in.
2016-08-23
==========
A few weeks ago, the previous Google Drive player stopped working. This is
nothing new; Google Drive has consistently broken a few times a year ever since
support for it was added. However, it's becoming increasingly difficult and
complicated to provide good support for Google Drive, so I've made the decision
to phase out the native player and require a userscript for it, in order to
bypass CORS and allow each browser to request the video stream itself.
See [the updated documentation](docs/gdrive-userscript-serveradmins.md) for
details on how to enable this for your users.
2016-04-27
==========
A new dependency has been added on `cytube-common`, a module that will hold
common code shared between the current version of CyTube and the upcoming work
around splitting it into multiple services. You will need to be sure to run
`npm install` after pulling in this change to pull in the new dependency.
2016-01-06
==========
This release updates socket.io to version 1.4.0. The updates to socket.io
include a few security-related fixes, so please be sure to run `npm install`
to ensure the updated version is installed before restarting your CyTube server.
* https://nodesecurity.io/advisories/67
* https://github.com/socketio/engine.io/commit/391ce0dc8b88a6609d88db83ea064040a05ab803
2015-10-25
==========
In order to support future clustering support, the legacy `/sioconfig`
endpoint is being deprecated. Instead, you should make a request to
`/socketconfig/<channel name>.json`. See [the
documentation](docs/socketconfig.md) for more information.
2015-10-04
==========
* The channel data storage system has been refactored a bit. For
compatibility, the default remains to store JSON objects for each channel in
the `chandump` folder, however there is now also the option of storing
channel data in the database. You can take advantage of this by setting
`channel-storage: type: 'database'` in your `config.yaml`.
- In order to migrate existing channel data from the `chandump` files to the
database, run `node lib/channel-storage/migrate.js`.
* The database storage method uses foreign keys to associate the channel data
with the corresponding row in the `channels` table. This requires that the
tables be stored using the InnoDB engine rather than MyISAM. If your CyTube
tables defaulted to MyISAM, you can fix them by running
```sql
ALTER TABLE `channels` ENGINE = InnoDB;
```
2015-09-21
==========
* CyTube is now transpiled with [babel] to allow the use of ES6/ES2015
features. All source files have been moved from `lib` to `src`.
* Running `npm install` or `npm run postinstall` will prompt you to
build from `src` to `lib`.
* Running `npm run build-server` will run the build script without any
prompts.
* After updating with `git pull`, you should run `npm install` or `npm run
build-server` in order to rebuild after the changes.
[babel]: https://babeljs.io/
2015-07-25
==========
* CyTube now supports subtitles for Google Drive videos. In order to take
advantage of this, you must upgrade mediaquery by running `npm install
cytube/mediaquery`. Subtitles are cached in the google-drive-subtitles
folder.
2015-07-07
==========
* CyTube and CyTube/mediaquery have both been updated to use
calzoneman/status-message-polyfill to polyfill res.statusMessage on older
versions of node (e.g., v0.10). After pulling, run `npm install` to update
this dependency. This fixes an issue where HTTP status messages from
mediaquery were reported as `undefined`, and removes the need for manually
looking up status messages in `lib/ffmpeg.js`.
2015-07-06
==========
* As part of the video player rewrite, Google Drive and Google+ metadata
lookups are now offloaded to CyTube/mediaquery. After pulling the new
changes, run `npm install` or `npm update` to update the mediaquery
dependency.
* `www/js/player.js` is now built from the CoffeeScript source files in the
`player/` directory. Instead of modifying it directly, modify the relevant
player implementations in `player/` and run `npm run build-player` (or `node
build-player.js`) to generate `www/js/player.js`.
* Also as part of the video player rewrite, the schema for custom embeds
changed so any custom embeds stored in the `channel_libraries` table need to
be updated. The automatic upgrade script will convert any custom embeds
that are parseable (i.e., not truncated by the width of the `id` field using
the old format) and will delete the rest (you may see a lot of WARNING:
unable to convert xxx messages-- this is normal). Custom embeds in channel
playlists in the chandumps will be converted when the channel is loaded.

View file

@ -1,77 +1,46 @@
Read before submitting an issue: https://github.com/calzoneman/sync/wiki/Reporting-an-Issue
===========================================================================================
CyTube
======
calzoneman/sync
===============
CyTube is a project I started in early 2013 as a hobby project to build my own
clone of synchtube.com (which shut down in March 2013).
About
-----
The basic concept is that users register channels where connected viewers can
watch videos from different video hosts (e.g., YouTube, Twitch) and the playback
is synchronized for all the viewers in the channel.
CyTube (formerly Sync) is a server/client combination providing media synchronization, chat,
and administration for an arbitrary number of channels.
I began developing this as a hobby project, and when Synchtube announced their closure, I
began polishing it and readying it for the public.
Each channel has a playlist where users can queue up videos to play, as well as
an integrated chatroom for discussion.
I am hosting a CyTube server at http://cytu.be
The official server is located at https://cytu.be, but there are other public
servers hosted for various communities.
The serverside is written in JavaScript and runs on Node.JS. It makes use
of a MySQL database to store user registrations, cached media metadata, and
data about each channel.
## Installation
The clientside is written in JavaScript and makes use of Socket.IO and
jQuery as well as the APIs for various media providers.
The web interface uses Bootstrap for layout and styling.
The installation guide for server administrators is located [on the
wiki](https://github.com/calzoneman/sync/wiki/CyTube-3.0-Installation-Guide).
The following media sources are currently supported:
- YouTube (individual videos)
- YouTube Playlists
- Vimeo
- Dailymotion
- Soundcloud
- Livestream.com
- Twitch.tv
- Justin.tv
- Ustream
- RTMP livestreams
## Contact
Installing
----------
**Please check if the
[FAQ](https://github.com/calzoneman/sync/wiki/Frequently-Asked-Questions)
answers your question already.**
Installation instructions for specific distributions are available here: https://github.com/calzoneman/sync/wiki/Installing
For bug reports and feature requests, please open a GitHub issue. To report a
security vulnerability, or to discuss an issue with https://cytu.be itself
(unrelated to the code), please send me an email: cyzon@cytu.be
This assumes you have Node.JS installed.
I'm using v0.10, please feel free to report which versions do/do not work.
I recommend using at least v0.8.20 due to a bug in previous versions of node
that caused sketchy client connections to crash the server.
Please be courteous and search through [the open and closed
issues](https://github.com/calzoneman/sync/issues?utf8=%E2%9C%93&q=is%3Aissue)
for your request before submitting a new one.
First install MySQL on the server. There are many online tutorials for setting up MySQL on
various operating systems.
I recommend installing phpMyAdmin so that you have a nice database administration interface.
Create a new user and database, and make sure the user has full permissions for the database.
General help with the software and the website is also available on the IRC
channel at [irc.esper.net#cytube](http://webchat.esper.net/?channels=cytube)
during US daytime hours.
Then, follow these instructions to install CyTube:
## License
1. Clone this repository (`git clone https://github.com/calzoneman/sync`)
2. cd to the directory containing the source files
3. Install your distribution's `libmysqlclient` package.
3. Install dependencies: `npm install`
4. Edit `config.js` and input your database details and connection port. Optionally, configure an SMTP transport to use for sending password reset emails (see https://github.com/andris9/Nodemailer).
5. Edit `www/assets/js/iourl.js` and change the value of `IO_URL` to `yourhostname:port` where `port` is the port defined in `config.js`. Also change `WEB_URL` to `yourhostname:web_port` where `web_port` is the websocket port you defined in `config.js`
Original source code in this repository is provided under the MIT license
(see the LICENSE file for the full text).
Running
-------
Start the server: `node server.js`
You should now be able to connect via `yourhostname:port` where `port` is
the port you defined in config.js
Feedback
--------
Please open a GitHub Issue.
License
-------
Licensed under MIT
See LICENSE for the full license text
Bundled source code, such as third-party CSS and JavaScript libraries, are
provided under their respective licenses.

209
acp.js
View file

@ -1,209 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var Auth = require("./auth");
var ActionLog = require("./actionlog");
module.exports = function (Server) {
return {
init: function(user) {
ActionLog.record(user.ip, user.name, "acp-init");
user.socket.on("acp-announce", function(data) {
ActionLog.record(user.ip, user.name, "acp-announce", data);
Server.announcement = data;
Server.io.sockets.emit("announcement", data);
});
user.socket.on("acp-announce-clear", function() {
ActionLog.record(user.ip, user.name, "acp-announce-clear");
Server.announcement = null;
});
user.socket.on("acp-global-ban", function(data) {
ActionLog.record(user.ip, user.name, "acp-global-ban", data.ip);
Server.db.globalBanIP(data.ip, data.note);
user.socket.emit("acp-global-banlist", Server.db.refreshGlobalBans());
});
user.socket.on("acp-global-unban", function(ip) {
ActionLog.record(user.ip, user.name, "acp-global-unban", ip);
Server.db.globalUnbanIP(ip);
user.socket.emit("acp-global-banlist", Server.db.refreshGlobalBans());
});
user.socket.emit("acp-global-banlist", Server.db.refreshGlobalBans());
user.socket.on("acp-lookup-user", function(name) {
var db = Server.db.getConnection();
if(!db) {
return;
}
var query = Server.db.createQuery(
"SELECT id,uname,global_rank,profile_image,profile_text,email FROM registrations WHERE uname LIKE ?",
["%"+name+"%"]
);
var res = db.querySync(query);
if(!res)
return;
var rows = res.fetchAllSync();
user.socket.emit("acp-userdata", rows);
});
user.socket.on("acp-lookup-channel", function (data) {
var db = Server.db.getConnection();
if(!db) {
return;
}
var query;
if(data.field === "owner") {
query = Server.db.createQuery(
"SELECT * FROM channels WHERE owner LIKE ?",
["%" + data.value + "%"]
);
} else if (data.field === "name") {
query = Server.db.createQuery(
"SELECT * FROM channels WHERE name LIKE ?",
["%" + data.value + "%"]
);
} else {
return;
}
var results = db.querySync(query);
if(!results)
return;
var rows = results.fetchAllSync();
user.socket.emit("acp-channeldata", rows);
});
user.socket.on("acp-reset-password", function(data) {
if(Auth.getGlobalRank(data.name) >= user.global_rank)
return;
try {
var hash = Server.db.generatePasswordReset(user.ip, data.name, data.email);
ActionLog.record(user.ip, user.name, "acp-reset-password", data.name);
}
catch(e) {
user.socket.emit("acp-reset-password", {
success: false,
error: e
});
return;
}
if(hash) {
user.socket.emit("acp-reset-password", {
success: true,
hash: hash
});
}
else {
user.socket.emit("acp-reset-password", {
success: false,
error: "Reset failed"
});
}
});
user.socket.on("acp-set-rank", function(data) {
if(data.rank < 1 || data.rank >= user.global_rank)
return;
if(Auth.getGlobalRank(data.name) >= user.global_rank)
return;
var db = Server.db.getConnection();
if(!db)
return;
ActionLog.record(user.ip, user.name, "acp-set-rank", data);
var query = Server.db.createQuery(
"UPDATE registrations SET global_rank=? WHERE uname=?",
[data.rank, data.name]
);
var res = db.querySync(query);
if(!res)
return;
user.socket.emit("acp-set-rank", data);
});
user.socket.on("acp-list-loaded", function() {
var chans = [];
var all = Server.channels;
for(var c in all) {
var chan = all[c];
chans.push({
name: chan.name,
title: chan.opts.pagetitle,
usercount: chan.users.length,
mediatitle: chan.playlist.current ? chan.playlist.current.media.title : "-",
is_public: chan.opts.show_public,
registered: chan.registered
});
}
user.socket.emit("acp-list-loaded", chans);
});
user.socket.on("acp-channel-unload", function(data) {
if(Server.channelLoaded(data.name)) {
var c = Server.getChannel(data.name);
if(!c)
return;
ActionLog.record(user.ip, user.name, "acp-channel-unload");
c.initialized = data.save;
c.users.forEach(function(u) {
c.kick(u, "Channel shutting down");
});
// At this point c should be unloaded
// if it's still loaded, kill it
if(Server.channelLoaded(data.name))
Server.unloadChannel(Server.getChannel(data.name));
}
});
user.socket.on("acp-actionlog-list", function () {
user.socket.emit("acp-actionlog-list",
ActionLog.getLogTypes()
);
});
user.socket.on("acp-actionlog-clear", function(data) {
ActionLog.clear(data);
ActionLog.record(user.ip, user.name, "acp-actionlog-clear", data);
});
user.socket.on("acp-actionlog-clear-one", function(data) {
ActionLog.clearOne(data);
ActionLog.record(user.ip, user.name, "acp-actionlog-clear-one", data);
});
user.socket.on("acp-view-stats", function () {
var db = Server.db.getConnection();
if(!db)
return;
var query = "SELECT * FROM stats ORDER BY time ASC";
var results = db.querySync(query);
if(results)
user.socket.emit("acp-view-stats", results.fetchAllSync());
});
}
}
}

View file

@ -1,151 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var Database = require("./database");
var Logger = require("./logger");
exports.record = function(ip, name, action, args) {
if(typeof args === "undefined" || args === null) {
args = "";
} else {
try {
args = JSON.stringify(args);
} catch(e) {
args = "";
}
}
var db = Database.getConnection();
if(!db)
return false;
var query = Database.createQuery(
"INSERT INTO actionlog (ip, name, action, args, time) "+
"VALUES (?, ?, ?, ?, ?)",
[ip, name, action, args, Date.now()]
);
var result = db.querySync(query);
if(!result) {
Logger.errlog.log("! Failed to record action");
}
return result;
}
exports.clear = function(actions) {
var db = Database.getConnection();
if(!db)
return false;
var list = new Array(actions.length);
for(var i = 0; i < actions.length; i++)
list[i] = "?";
var query = Database.createQuery(
"DELETE FROM actionlog WHERE action IN ("+
list.join(",")+
")",
actions
);
var result = db.querySync(query);
if(!result) {
Logger.errlog.log("! Failed to clear action log");
}
return result;
}
exports.clearOne = function(e) {
var db = Database.getConnection();
if(!db)
return false;
var query = Database.createQuery(
"DELETE FROM actionlog WHERE ip=? AND time=?",
[e.ip, e.time]
);
var result = db.querySync(query);
if(!result) {
Logger.errlog.log("! Failed to clear action log");
}
return result;
}
exports.tooManyRegistrations = function (ip) {
var db = Database.getConnection();
if(!db)
return true;
var query = Database.createQuery(
"SELECT * FROM actionlog WHERE ip=? AND action='register-success'"+
"AND time > ?",
[ip, Date.now() - 48 * 3600 * 1000]
);
var results = db.querySync(query);
if(!results) {
Logger.errlog.log("! Failed to check tooManyRegistrations");
return true;
}
var rows = results.fetchAllSync();
// TODO Config value for this
return rows.length > 4;
}
exports.getLogTypes = function () {
var db = Database.getConnection();
if(!db)
return false;
var query = "SELECT DISTINCT action FROM actionlog";
var result = db.querySync(query);
if(!result) {
Logger.errlog.log("! Failed to read action log");
return [];
}
result = result.fetchAllSync();
var actions = [];
for(var i in result)
actions.push(result[i].action);
return actions;
}
exports.readLog = function (actions) {
var db = Database.getConnection();
if(!db)
return false;
var query = "SELECT * FROM actionlog";
if(actions !== undefined) {
var list = new Array(actions.length);
for(var i in actions)
list[i] = "?";
list = list.join(",");
query += Database.createQuery(
" WHERE action IN ("+list+")",
actions
);
}
var result = db.querySync(query);
if(!result) {
Logger.errlog.log("! Failed to read action log");
return [];
}
return result.fetchAllSync();
}

619
api.js
View file

@ -1,619 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var Auth = require("./auth");
var Logger = require("./logger");
var apilog = new Logger.Logger("api.log");
var ActionLog = require("./actionlog");
var fs = require("fs");
module.exports = function (Server) {
function getIP(req) {
var raw = req.connection.remoteAddress;
var forward = req.header("x-forwarded-for");
if(Server.cfg["trust-x-forward"] && forward) {
var ip = forward.split(",")[0];
Logger.syslog.log("REVPROXY " + raw + " => " + ip);
return ip;
}
return raw;
}
var API = function () {
}
API.prototype = {
handle: function (path, req, res) {
var parts = path.split("/");
var last = parts[parts.length - 1];
var params = {};
if(last.indexOf("?") != -1) {
parts[parts.length - 1] = last.substring(0, last.indexOf("?"));
var plist = last.substring(last.indexOf("?") + 1).split("&");
for(var i = 0; i < plist.length; i++) {
var kv = plist[i].split("=");
if(kv.length != 2) {
res.send(400);
return;
}
params[unescape(kv[0])] = unescape(kv[1]);
}
}
for(var i = 0; i < parts.length; i++) {
parts[i] = unescape(parts[i]);
}
if(parts.length != 2) {
res.send(400);
return;
}
if(parts[0] == "json") {
res.callback = params.callback || false;
if(!(parts[1] in this.jsonHandlers)) {
res.end(JSON.stringify({
error: "Unknown endpoint: " + parts[1]
}, null, 4));
return;
}
this.jsonHandlers[parts[1]](params, req, res);
}
else if(parts[0] == "plain") {
if(!(parts[1] in this.plainHandlers)) {
res.send(404);
return;
}
this.plainHandlers[parts[1]](params, req, res);
}
else {
res.send(400);
}
},
sendJSON: function (res, obj) {
var response = JSON.stringify(obj, null, 4);
if(res.callback) {
response = res.callback + "(" + response + ")";
}
var len = unescape(encodeURIComponent(response)).length;
res.setHeader("Content-Type", "application/json");
res.setHeader("Content-Length", len);
res.end(response);
},
sendPlain: function (res, str) {
if(res.callback) {
str = res.callback + "('" + str + "')";
}
var len = unescape(encodeURIComponent(str)).length;
res.setHeader("Content-Type", "text/plain");
res.setHeader("Content-Length", len);
res.end(response);
},
handleChannelData: function (params, req, res) {
var clist = params.channel || "";
clist = clist.split(",");
var data = [];
for(var j = 0; j < clist.length; j++) {
var cname = clist[j];
if(!cname.match(/^[a-zA-Z0-9-_]+$/)) {
continue;
}
var d = {
name: cname,
loaded: Server.channelLoaded(cname)
};
if(d.loaded) {
var chan = Server.getChannel(cname);
d.pagetitle = chan.opts.pagetitle;
d.media = chan.playlist.current ? chan.playlist.current.media.pack() : {};
d.usercount = chan.users.length;
d.users = [];
for(var i = 0; i < chan.users.length; i++) {
if(chan.users[i].name) {
d.users.push(chan.users[i].name);
}
}
d.chat = [];
for(var i = 0; i < chan.chatbuffer.length; i++) {
d.chat.push(chan.chatbuffer[i]);
}
}
data.push(d);
}
this.sendJSON(res, data);
},
handleChannelList: function (params, req, res) {
if(params.filter == "public") {
var all = Server.channels;
var clist = [];
for(var key in all) {
if(all[key].opts.show_public) {
clist.push(all[key].name);
}
}
this.handleChannelData({channel: clist.join(",")}, req, res);
}
var session = params.session || "";
var name = params.name || "";
var pw = params.pw || "";
var row = Auth.login(name, pw, session);
if(!row || row.global_rank < 255) {
res.send(403);
return;
}
var clist = [];
for(var key in Server.channels) {
clist.push(Server.channels[key].name);
}
this.handleChannelData({channel: clist.join(",")}, req, res);
},
handleLogin: function (params, req, res) {
var session = params.session || "";
var name = params.name || "";
var pw = params.pw || "";
if(pw == "" && session == "") {
if(!Auth.isRegistered(name)) {
this.sendJSON(res, {
success: true,
session: ""
});
return;
}
else {
this.sendJSON(res, {
success: false,
error: "That username is already taken"
});
return;
}
}
var row = Auth.login(name, pw, session);
if(row) {
if(row.global_rank >= 255)
ActionLog.record(getIP(req), name, "login-success");
this.sendJSON(res, {
success: true,
session: row.session_hash
});
}
else {
ActionLog.record(getIP(req), name, "login-failure");
this.sendJSON(res, {
error: "Invalid username/password",
success: false
});
}
},
handlePasswordChange: function (params, req, res) {
var name = params.name || "";
var oldpw = params.oldpw || "";
var newpw = params.newpw || "";
if(oldpw == "" || newpw == "") {
this.sendJSON(res, {
success: false,
error: "Old password and new password cannot be empty"
});
return;
}
var row = Auth.login(name, oldpw);
if(row) {
ActionLog.record(getIP(req), name, "password-change");
var success = Auth.setUserPassword(name, newpw);
this.sendJSON(res, {
success: success,
error: success ? "" : "Change password failed",
session: row.session_hash
});
}
else {
this.sendJSON(res, {
success: false,
error: "Invalid username/password"
});
}
},
handlePasswordReset: function (params, req, res) {
var name = params.name || "";
var email = params.email || "";
var ip = getIP(req);
var hash = false;
try {
hash = Server.db.generatePasswordReset(ip, name, email);
ActionLog.record(ip, name, "password-reset-generate", email);
}
catch(e) {
this.sendJSON(res, {
success: false,
error: e
});
return;
}
if(!Server.cfg["enable-mail"]) {
this.sendJSON(res, {
success: false,
error: "This server does not have email enabled. Contact an administrator"
});
return;
}
if(!email) {
this.sendJSON(res, {
success: false,
error: "You don't have a recovery email address set. Contact an administrator"
});
return;
}
var msg = [
"A password reset request was issued for your account `",
name,
"` on ",
Server.cfg["domain"],
". This request is valid for 24 hours. ",
"If you did not initiate this, there is no need to take action. ",
"To reset your password, copy and paste the following link into ",
"your browser: ",
Server.cfg["domain"],
"/reset.html?",
hash
].join("");
var mail = {
from: "CyTube Services <" + Server.cfg["mail-from"] + ">",
to: email,
subject: "Password reset request",
text: msg
};
var api = this;
Server.cfg["nodemailer"].sendMail(mail, function(err, response) {
if(err) {
Logger.errlog.log("Mail fail: " + err);
api.sendJSON(res, {
success: false,
error: "Email failed. Contact an admin if this persists."
});
}
else {
api.sendJSON(res, {
success: true
});
if(Server.cfg["debug"]) {
Logger.syslog.log(response);
}
}
});
},
handlePasswordRecover: function (params, req, res) {
var hash = params.hash || "";
var ip = getIP(req);
try {
var info = Server.db.recoverPassword(hash);
this.sendJSON(res, {
success: true,
name: info[0],
pw: info[1]
});
ActionLog.record(ip, info[0], "password-recover-success");
Logger.syslog.log(ip + " recovered password for " + info[0]);
return;
}
catch(e) {
ActionLog.record(ip, "", "password-recover-failure");
this.sendJSON(res, {
success: false,
error: e
});
}
},
handleProfileGet: function (params, req, res) {
var name = params.name || "";
try {
var prof = Server.db.getProfile(name);
this.sendJSON(res, {
success: true,
profile_image: prof.profile_image,
profile_text: prof.profile_text
});
}
catch(e) {
this.sendJSON(res, {
success: false,
error: e
});
}
},
handleProfileChange: function (params, req, res) {
var name = params.name || "";
var pw = params.pw || "";
var session = params.session || "";
var img = params.profile_image || "";
var text = params.profile_text || "";
var row = Auth.login(name, pw, session);
if(!row) {
this.sendJSON(res, {
success: false,
error: "Invalid login"
});
return;
}
var result = Server.db.setProfile(name, {
image: img,
text: text
});
this.sendJSON(res, {
success: result,
error: result ? "" : "Internal error. Contact an administrator"
});
var all = Server.channels;
for(var n in all) {
var chan = all[n];
for(var i = 0; i < chan.users.length; i++) {
if(chan.users[i].name.toLowerCase() == name) {
chan.users[i].profile = {
image: img,
text: text
};
chan.broadcastUserUpdate(chan.users[i]);
break;
}
}
}
},
handleEmailChange: function (params, req, res) {
var name = params.name || "";
var pw = params.pw || "";
var email = params.email || "";
// perhaps my email regex isn't perfect, but there's no freaking way
// I'm implementing this monstrosity:
// <http://www.ex-parrot.com/pdw/Mail-RFC822-Address.html>
if(!email.match(/^[a-z0-9_\.]+@[a-z0-9_\.]+[a-z]+$/)) {
this.sendJSON(res, {
success: false,
error: "Invalid email"
});
return;
}
if(email.match(/.*@(localhost|127\.0\.0\.1)/i)) {
this.sendJSON(res, {
success: false,
error: "Nice try, but no."
});
return;
}
if(pw == "") {
this.sendJSON(res, {
success: false,
error: "Password cannot be empty"
});
return;
}
var row = Auth.login(name, pw);
if(row) {
var success = Server.db.setUserEmail(name, email);
ActionLog.record(getIP(req), name, "email-update", email);
this.sendJSON(res, {
success: success,
error: success ? "" : "Email update failed",
session: row.session_hash
});
}
else {
this.sendJSON(res, {
success: false,
error: "Invalid username/password"
});
}
},
handleRegister: function (params, req, res) {
var name = params.name || "";
var pw = params.pw || "";
if(ActionLog.tooManyRegistrations(getIP(req))) {
ActionLog.record(getIP(req), name, "register-failure",
"Too many recent registrations from this IP");
this.sendJSON(res, {
success: false,
error: "Your IP address has registered several accounts in "+
"the past 48 hours. Please wait a while or ask an "+
"administrator for assistance."
});
return;
}
if(pw == "") {
this.sendJSON(res, {
success: false,
error: "You must provide a password"
});
return;
}
else if(Auth.isRegistered(name)) {
ActionLog.record(getIP(req), name, "register-failure",
"Name taken");
this.sendJSON(res, {
success: false,
error: "That username is already taken"
});
return false;
}
else if(!Auth.validateName(name)) {
ActionLog.record(getIP(req), name, "register-failure",
"Invalid name");
this.sendJSON(res, {
success: false,
error: "Invalid username. Usernames must be 1-20 characters long and consist only of alphanumeric characters and underscores"
});
}
else {
var session = Auth.register(name, pw);
if(session) {
ActionLog.record(getIP(req), name, "register-success");
Logger.syslog.log(getIP(req) + " registered " + name);
this.sendJSON(res, {
success: true,
session: session
});
}
else {
this.sendJSON(res, {
success: false,
error: "Registration error. Contact an admin for assistance."
});
}
}
},
handleListUserChannels: function (params, req, res) {
var name = params.name || "";
var pw = params.pw || "";
var session = params.session || "";
var row = Auth.login(name, pw, session);
if(!row) {
this.sendJSON(res, {
success: false,
error: "Invalid login"
});
return;
}
var channels = Server.db.listUserChannels(name);
this.sendJSON(res, {
success: true,
channels: channels
});
},
handleAdmReports: function (params, req, res) {
this.sendJSON(res, {
error: "Not implemented"
});
},
handleReadActionLog: function (params, req, res) {
var name = params.name || "";
var pw = params.pw || "";
var session = params.session || "";
var types = params.actions || "";
var row = Auth.login(name, pw, session);
if(!row || row.global_rank < 255) {
res.send(403);
return;
}
var actiontypes = types.split(",");
var actions = ActionLog.readLog(actiontypes);
this.sendJSON(res, actions);
},
// Helper function
pipeLast: function (res, file, len) {
fs.stat(file, function(err, data) {
if(err) {
res.send(500);
return;
}
var start = data.size - len;
if(start < 0) {
start = 0;
}
var end = data.size - 1;
fs.createReadStream(file, {start: start, end: end}).pipe(res);
});
},
handleReadLog: function (params, req, res) {
var name = params.name || "";
var pw = params.pw || "";
var session = params.session || "";
var row = Auth.login(name, pw, session);
if(!row || row.global_rank < 255) {
res.send(403);
return;
}
res.setHeader("Access-Control-Allow-Origin", "*");
var type = params.type || "";
if(type == "sys") {
this.pipeLast(res, "sys.log", 1024*1024);
}
else if(type == "err") {
this.pipeLast(res, "error.log", 1024*1024);
}
else if(type == "channel") {
var chan = params.channel || "";
fs.exists("chanlogs/" + chan + ".log", function(exists) {
if(exists) {
this.pipeLast(res, "chanlogs/" + chan + ".log", 1024*1024);
}
else {
res.send(404);
}
}.bind(this));
}
else {
res.send(400);
}
}
};
var api = new API();
api.plainHandlers = {
"readlog" : api.handleReadLog.bind(api)
};
api.jsonHandlers = {
"channeldata" : api.handleChannelData.bind(api),
"listloaded" : api.handleChannelList.bind(api),
"login" : api.handleLogin.bind(api),
"register" : api.handleRegister.bind(api),
"changepass" : api.handlePasswordChange.bind(api),
"resetpass" : api.handlePasswordReset.bind(api),
"recoverpw" : api.handlePasswordRecover.bind(api),
"setprofile" : api.handleProfileChange.bind(api),
"getprofile" : api.handleProfileGet.bind(api),
"listuserchannels": api.handleListUserChannels.bind(api),
"setemail" : api.handleEmailChange.bind(api),
"admreports" : api.handleAdmReports.bind(api),
"readactionlog" : api.handleReadActionLog.bind(api)
};
return api;
}

226
auth.js
View file

@ -1,226 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var mysql = require("mysql-libmysqlclient");
var Database = require("./database.js");
var bcrypt = require("bcrypt");
var hashlib = require("node_hash");
var Logger = require("./logger.js");
// Check if a name is taken
exports.isRegistered = function(name) {
var db = Database.getConnection();
if(!db) {
throw "Database failure";
}
var query = Database.createQuery(
"SELECT * FROM `registrations` WHERE uname=?",
[name]
);
var results = db.querySync(query);
if(!results) {
return true;
}
var rows = results.fetchAllSync();
return rows.length > 0;
}
// Check if a name is valid
// Valid names are 1-20 characters, alphanumeric and underscores
exports.validateName = function(name) {
if(name.length > 20)
return false;
const VALID_REGEX = /^[a-zA-Z0-9_]+$/;
return name.match(VALID_REGEX) != null;
}
// Try to register a new account
exports.register = function(name, pw) {
if(!exports.validateName(name))
return false;
if(exports.isRegistered(name))
return false;
var db = Database.getConnection();
if(!db) {
return false;
}
var hash = bcrypt.hashSync(pw, 10);
var query = Database.createQuery(
["INSERT INTO `registrations` VALUES ",
"(NULL, ?, ?, 1, '', 0, '', '', '')"].join(""),
[name, hash]
);
var results = db.querySync(query);
if(results) {
return exports.createSession(name);
}
return false;
}
exports.login = function(name, pw, session) {
if(session) {
var res = exports.loginSession(name, session);
if(res) {
return res;
}
else if(!pw) {
return false;
}
}
var row = exports.loginPassword(name, pw);
if(row) {
var hash = exports.createSession(name);
row.session_hash = hash;
return row;
}
}
// Try to login
exports.loginPassword = function(name, pw) {
var db = Database.getConnection();
if(!db) {
throw "Database failure";
}
var query = Database.createQuery(
"SELECT * FROM `registrations` WHERE uname=?",
[name]
);
var results = db.querySync(query);
if(!results) {
return false;
}
var rows = results.fetchAllSync();
if(rows.length > 0) {
try {
if(bcrypt.compareSync(pw, rows[0].pw)) {
return rows[0];
}
else {
// Check if the sha256 is in the database
// If so, migrate to bcrypt
var sha256 = hashlib.sha256(pw);
if(sha256 == rows[0].pw) {
var newhash = bcrypt.hashSync(pw, 10);
var query = Database.createQuery(
["UPDATE `registrations` SET pw=?",
"WHERE uname=?"].join(""),
[newhash, name]
);
var results = db.querySync(query);
if(!results) {
Logger.errlog.log("Failed to migrate password! user=" + name);
return false;
}
return rows[0];
}
return false;
}
}
catch(e) {
Logger.errlog.log("Auth.login fail");
Logger.errlog.log(e);
}
}
return false;
}
exports.createSession = function(name) {
var salt = sessionSalt();
var hash = hashlib.sha256(salt + name);
var db = Database.getConnection();
if(!db) {
throw "Database failure";
}
var query = Database.createQuery(
["UPDATE `registrations` SET ",
"`session_hash`=?,",
"`expire`=? ",
"WHERE uname=?"].join(""),
[hash, Date.now() + 604800000, name]
);
var results = db.querySync(query);
return results ? hash : false;
}
exports.loginSession = function(name, hash) {
var db = Database.getConnection();
if(!db) {
throw "Database failure";
}
var query = Database.createQuery(
"SELECT * FROM `registrations` WHERE `uname`=?",
[name]
);
var results = db.querySync(query);
if(!results) {
return false;
}
var rows = results.fetchAllSync();
if(rows.length != 1) {
return false;
}
var dbhash = rows[0].session_hash;
if(hash != dbhash) {
return false;
}
var timeout = rows[0].expire;
if(timeout < new Date().getTime()) {
return false;
}
return rows[0];
}
function sessionSalt() {
var chars = "abcdefgihjklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789!@#$%^&*_+=~";
var salt = [];
for(var i = 0; i < 32; i++) {
salt.push(chars[parseInt(Math.random()*chars.length)]);
}
return salt.join('');
}
exports.setUserPassword = function(name, pw) {
var db = Database.getConnection();
if(!db) {
return false;
}
var hash = bcrypt.hashSync(pw, 10);
var query = Database.createQuery(
"UPDATE `registrations` SET `pw`=? WHERE `uname`=?",
[hash, name]
);
var result = db.querySync(query);
return result;
}
exports.getGlobalRank = function(name) {
var db = Database.getConnection();
if(!db) {
return false;
}
var query = Database.createQuery(
"SELECT * FROM `registrations` WHERE `uname`=?",
[name]
);
var results = db.querySync(query);
if(!results) {
return 0;
}
var rows = results.fetchAllSync();
if(rows.length > 0) {
return rows[0].global_rank;
}
return 0;
}

176
bin/admin.js Executable file
View file

@ -0,0 +1,176 @@
#!/usr/bin/env node
const Config = require('../lib/config');
Config.load('config.yaml');
if (!Config.get('service-socket.enabled')){
console.error('The Service Socket is not enabled.');
process.exit(1);
}
const net = require('net');
const path = require('path');
const readline = require('node:readline/promises');
const socketPath = path.resolve(__dirname, '..', Config.get('service-socket.socket'));
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
async function doCommand(params) {
return new Promise((resolve, reject) => {
const client = net.createConnection(socketPath);
client.on('connect', () => {
client.write(JSON.stringify(params) + '\n');
});
client.on('data', data => {
client.end();
resolve(JSON.parse(data));
});
client.on('error', error => {
reject(error);
});
});
}
let commands = [
{
command: 'ban-channel',
handler: async args => {
if (args.length !== 3) {
console.log('Usage: ban-channel <name> <externalReason> <internalReason>');
process.exit(1);
}
let [name, externalReason, internalReason] = args;
let answer = await rl.question(`Ban ${name} with external reason "${externalReason}" and internal reason "${internalReason}"? `);
if (!/^[yY]$/.test(answer)) {
console.log('Aborted.');
process.exit(1);
}
let res = await doCommand({
command: 'ban-channel',
name,
externalReason,
internalReason
});
switch (res.status) {
case 'error':
console.log('Error:', res.error);
process.exit(1);
break;
case 'success':
console.log('Ban succeeded.');
process.exit(0);
break;
default:
console.log(`Unknown result: ${res.status}`);
process.exit(1);
break;
}
}
},
{
command: 'unban-channel',
handler: async args => {
if (args.length !== 1) {
console.log('Usage: unban-channel <name>');
process.exit(1);
}
let [name] = args;
let answer = await rl.question(`Unban ${name}? `);
if (!/^[yY]$/.test(answer)) {
console.log('Aborted.');
process.exit(1);
}
let res = await doCommand({
command: 'unban-channel',
name
});
switch (res.status) {
case 'error':
console.log('Error:', res.error);
process.exit(1);
break;
case 'success':
console.log('Unban succeeded.');
process.exit(0);
break;
default:
console.log(`Unknown result: ${res.status}`);
process.exit(1);
break;
}
}
},
{
command: 'show-banned-channel',
handler: async args => {
if (args.length !== 1) {
console.log('Usage: show-banned-channel <name>');
process.exit(1);
}
let [name] = args;
let res = await doCommand({
command: 'show-banned-channel',
name
});
switch (res.status) {
case 'error':
console.log('Error:', res.error);
process.exit(1);
break;
case 'success':
if (res.ban != null) {
console.log(`Channel: ${name}`);
console.log(`Ban issued: ${res.ban.createdAt}`);
console.log(`Banned by: ${res.ban.bannedBy}`);
console.log(`External reason:\n${res.ban.externalReason}`);
console.log(`Internal reason:\n${res.ban.internalReason}`);
} else {
console.log(`Channel ${name} is not banned.`);
}
process.exit(0);
break;
default:
console.log(`Unknown result: ${res.status}`);
process.exit(1);
break;
}
}
}
];
let found = false;
commands.forEach(cmd => {
if (cmd.command === process.argv[2]) {
found = true;
cmd.handler(process.argv.slice(3)).then(() => {
process.exit(0);
}).catch(error => {
console.log('Error in command:', error.stack);
});
}
});
if (!found) {
console.log('Available commands:');
commands.forEach(cmd => {
console.log(` * ${cmd.command}`);
});
process.exit(1);
}

52
bin/build-player.js Executable file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env node
var coffee = require('coffeescript');
var fs = require('fs');
var path = require('path');
var order = [
'base.coffee',
'dailymotion.coffee',
'niconico.coffee',
'peertube.coffee',
'soundcloud.coffee',
'twitch.coffee',
'vimeo.coffee',
'youtube.coffee',
// playerjs-based players
'playerjs.coffee',
'iframechild.coffee',
'odysee.coffee',
'whepplayer.coffee',
'streamable.coffee',
// iframe embed-based players
'embed.coffee',
'custom-embed.coffee',
'livestream.com.coffee',
'twitchclip.coffee',
// video.js-based players
'videojs.coffee',
'gdrive-player.coffee',
'hls.coffee',
'raw-file.coffee',
'rtmp.coffee',
// mediaUpdate handler
'update.coffee'
];
var buffer = '';
order.forEach(function (file) {
buffer += fs.readFileSync(
path.join(__dirname, '..', 'player', file)
) + '\n';
});
fs.writeFileSync(
path.join(__dirname, '..', 'www', 'js', 'player.js'),
coffee.compile(buffer)
);

2026
channel.js

File diff suppressed because it is too large Load diff

View file

@ -1,233 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var Rank = require("./rank.js");
var Poll = require("./poll.js").Poll;
var Logger = require("./logger.js");
function handle(chan, user, msg, data) {
if(msg.indexOf("/me ") == 0)
chan.sendMessage(user.name, msg.substring(4), "action", data);
else if(msg.indexOf("/sp ") == 0)
chan.sendMessage(user.name, msg.substring(4), "spoiler", data);
else if(msg.indexOf("/say ") == 0) {
if(Rank.hasPermission(user, "shout") || chan.leader == user) {
chan.sendMessage(user.name, msg.substring(5), "shout", data);
}
}
else if(msg.indexOf("/afk") == 0) {
user.setAFK(!user.meta.afk);
}
else if(msg.indexOf("/m ") == 0) {
if(user.rank >= Rank.Moderator) {
chan.chainMessage(user, msg.substring(3), {modflair: user.rank})
}
}
else if(msg.indexOf("/a ") == 0) {
if(user.rank >= Rank.Siteadmin) {
var flair = {
superadminflair: {
labelclass: "label-important",
icon: "icon-globe"
}
};
var args = msg.substring(3).split(" ");
var cargs = [];
for(var i = 0; i < args.length; i++) {
var a = args[i];
if(a.indexOf("!icon-") == 0)
flair.superadminflair.icon = a.substring(1);
else if(a.indexOf("!label-") == 0)
flair.superadminflair.labelclass = a.substring(1);
else {
cargs.push(a);
}
}
chan.chainMessage(user, cargs.join(" "), flair);
}
}
else if(msg.indexOf("/mute ") == 0) {
handleMute(chan, user, msg.substring(6).split(" "));
}
else if(msg.indexOf("/unmute ") == 0) {
handleUnmute(chan, user, msg.substring(8).split(" "));
}
else if(msg.indexOf("/kick ") == 0) {
handleKick(chan, user, msg.substring(6).split(" "));
}
else if(msg.indexOf("/ban ") == 0) {
handleBan(chan, user, msg.substring(5).split(" "));
}
else if(msg.indexOf("/ipban ") == 0) {
handleIPBan(chan, user, msg.substring(7).split(" "));
}
else if(msg.indexOf("/unban ") == 0) {
handleUnban(chan, user, msg.substring(7).split(" "));
}
else if(msg.indexOf("/poll ") == 0) {
handlePoll(chan, user, msg.substring(6));
}
else if(msg.indexOf("/d") == 0 && msg.length > 2 &&
msg[2].match(/[-0-9 ]/)) {
if(msg[2] == "-") {
if(msg.length == 3)
return;
if(!msg[3].match(/[0-9]/))
return;
}
handleDrink(chan, user, msg.substring(2), data);
}
else if(msg.indexOf("/clear") == 0) {
handleClear(chan, user);
}
}
function handleMute(chan, user, args) {
if(chan.hasPermission(user, "mute") && args.length > 0) {
args[0] = args[0].toLowerCase();
var person = false;
for(var i = 0; i < chan.users.length; i++) {
if(chan.users[i].name.toLowerCase() == args[0]) {
person = chan.users[i];
break;
}
}
if(person) {
if(person.rank >= user.rank) {
user.socket.emit("errorMsg", {
msg: "You don't have permission to mute that person."
});
return;
}
person.meta.icon = "icon-volume-off";
person.muted = true;
chan.broadcastUserUpdate(person);
chan.logger.log("*** " + user.name + " muted " + args[0]);
}
}
}
function handleUnmute(chan, user, args) {
if(chan.hasPermission(user, "mute") && args.length > 0) {
args[0] = args[0].toLowerCase();
var person = false;
for(var i = 0; i < chan.users.length; i++) {
if(chan.users[i].name.toLowerCase() == args[0]) {
person = chan.users[i];
break;
}
}
if(person) {
if(person.rank >= user.rank) {
user.socket.emit("errorMsg", {
msg: "You don't have permission to unmute that person."
});
return;
}
person.meta.icon = false;
person.muted = false;
chan.broadcastUserUpdate(person);
chan.logger.log("*** " + user.name + " unmuted " + args[0]);
}
}
}
function handleKick(chan, user, args) {
if(chan.hasPermission(user, "kick") && args.length > 0) {
args[0] = args[0].toLowerCase();
var kickee;
for(var i = 0; i < chan.users.length; i++) {
if(chan.users[i].name.toLowerCase() == args[0] &&
chan.getRank(chan.users[i].name) < user.rank) {
kickee = chan.users[i];
break;
}
}
if(kickee) {
chan.logger.log("*** " + user.name + " kicked " + args[0]);
args[0] = "";
var reason = args.join(" ");
chan.kick(kickee, reason);
}
}
}
function handleIPBan(chan, user, args) {
chan.tryIPBan(user, args[0], args[1]);
// Ban the name too for good measure
chan.tryNameBan(user, args[0]);
}
function handleBan(chan, user, args) {
chan.tryNameBan(user, args[0]);
}
function handleUnban(chan, user, args) {
if(chan.hasPermission(user, "ban") && args.length > 0) {
chan.logger.log("*** " + user.name + " unbanned " + args[0]);
if(args[0].match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/)) {
chan.unbanIP(user, args[0]);
}
else {
chan.unbanName(user, args[0]);
}
}
}
function handlePoll(chan, user, msg) {
if(chan.hasPermission(user, "pollctl")) {
var args = msg.split(",");
var title = args[0];
args.splice(0, 1);
var poll = new Poll(user.name, title, args);
chan.poll = poll;
chan.broadcastPoll();
chan.logger.log("*** " + user.name + " Opened Poll: '" + poll.title + "'");
}
}
function handleDrink(chan, user, msg, data) {
if(!chan.hasPermission(user, "drink")) {
return;
}
var count = msg.split(" ")[0];
msg = msg.substring(count.length + 1);
if(count == "")
count = 1;
else
count = parseInt(count);
chan.drinks += count;
chan.broadcastDrinks();
if(count < 0 && msg.trim() == "") {
return;
}
msg = msg + " drink!";
if(count != 1)
msg += " (x" + count + ")";
chan.sendMessage(user.name, msg, "drink", data);
}
function handleClear(chan, user) {
if(user.rank < Rank.Moderator) {
return;
}
chan.chatbuffer = [];
chan.sendAll("clearchat");
}
exports.handle = handle;

15
conf/example/camo.toml Normal file
View file

@ -0,0 +1,15 @@
# Configuration for proxying images to a camo (or camo-compatible) proxy server.
# To use, copy to conf/camo.toml.
# More info on camo: https://github.com/atmos/camo
[camo]
enabled = true
server = 'https://my-camo-server'
# The key must match the `CAMO_KEY` environment variable passed to the camo server.
key = 'ABCDEFGH'
# Bypass the proxy for domains you trust that already support HTTPS and won't be harmful to users.
whitelisted-domains = [
'i.imgur.com',
'i.4cdn.org'
]
# Whether to use URL encoding ("url") or hex encoding ("hex") for the target URL.
encoding = 'url'

View file

@ -0,0 +1,9 @@
[hcaptcha]
# Site key from hCaptcha. The value here by default is the dummy test key for local testing
site-key = "10000000-ffff-ffff-ffff-000000000001"
# Secret key from hCaptcha. The value here by default is the dummy test key for local testing
secret = "0x0000000000000000000000000000000000000000"
[register]
# Whether to require a captcha for registration
enabled = true

66
conf/example/email.toml Normal file
View file

@ -0,0 +1,66 @@
# SMTP configuration for sending mail
[smtp]
host = 'smtp.gmail.com'
port = 465
secure = true
user = 'some-user@example.com'
password = 'secretpassword'
# Email configuration for password reset emails
# Be sure to update both html-template AND text-template
# nodemailer will send both and the email client will render whichever one is supported
[password-reset]
enabled = true
# Template to use for HTML-formatted emails
# $user$ will be replaced by the username for which the reset was requested
# $url$ will be replaced by the password reset confirmation link
html-template = """
Hi $user$,<br>
<br>
A password reset was requested for your account on CHANGE ME. You can complete the reset by opening the following link in your browser: <a href="$url$">$url$</a><br>
<br>
This link will expire in 24 hours.<br>
<br>
This email address is not monitored for replies. For assistance with password resets, please <a href="http://example.com/contact">contact an administrator</a>.
"""
# Template to use for plaintext emails
# Same substitutions as the HTML template
text-template = """
Hi $user$,
A password reset was requested for your account on CHANGE ME. You can complete the reset by opening the following link in your browser: $url$
This link will expire in 24 hours.
This email address is not monitored for replies. For assistance with password resets, please contact an administrator. See http://example.com/contact for contact information.
"""
from = "Example Website <website@example.com>"
subject = "Password reset request"
# Email configuration for account deletion request notifications
[delete-account]
enabled = true
html-template = """
Hi $user$,
<br>
<br>
Account deletion was requested for your account on CHANGE ME. Your account will be automatically deleted in 7 days without any further action from you.
<br>
<br>
This email address is not monitored for replies. For assistance, please <a href="http://example.com/contact">contact an administrator</a>.
"""
text-template = """
Hi $user$,
Account deletion was requested for your account on CHANGE ME. Your account will be automatically deleted in 7 days without any further action from you.
This email address is not monitored for replies. For assistance, please contact an administrator. See http://example.com/contact for contact information.
"""
from = "Example Website <website@example.com>"
subject = "Account deletion request"

View file

@ -0,0 +1,14 @@
# Configuration for binding an HTTP server to export prometheus metrics.
# See https://prometheus.io/ and https://github.com/siimon/prom-client
# for more details.
[prometheus]
enabled = true
# Host, port to bind. This is separate from the main CyTube HTTP server
# because it may be desirable to bind a different IP/port for monitoring
# purposes. Default: localhost port 19820 (arbitrary port chosen not to
# conflict with existing prometheus exporters).
host = '127.0.0.1'
port = 19820
# Request path to serve metrics. All other paths are rejected with
# 400 Bad Request.
path = '/metrics'

View file

@ -1,99 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var fs = require("fs");
var Logger = require("./logger");
var nodemailer = require("nodemailer");
var defaults = {
"mysql-server" : "localhost",
"mysql-db" : "cytube",
"mysql-user" : "cytube",
"mysql-pw" : "supersecretpass",
"express-host" : "0.0.0.0",
"asset-cache-ttl" : 0,
"web-port" : 8080,
"io-port" : 1337,
"ip-connection-limit" : 10,
"guest-login-delay" : 60,
"trust-x-forward" : false,
"enable-mail" : false,
"mail-transport" : "SMTP",
"mail-config" : {
"service" : "Gmail",
"auth" : {
"user" : "some.user@gmail.com",
"pass" : "supersecretpassword"
}
},
"mail-from" : "some.user@gmail.com",
"domain" : "http://localhost",
"ytv3apikey" : "",
"enable-ytv3" : false,
"ytv2devkey" : ""
}
function save(cfg, file) {
var x = {};
for(var k in cfg) {
if(k !== "nodemailer")
x[k] = cfg[k];
}
fs.writeFile(file, JSON.stringify(x, null, 4), function (err) {
if(err) {
Logger.errlog.log("Failed to save config");
Logger.errlog.log(err);
}
});
}
exports.load = function (Server, file, callback) {
var cfg = {};
for(var k in defaults)
cfg[k] = defaults[k];
fs.readFile(file, function (err, data) {
if(err) {
if(err.code == "ENOENT") {
Logger.syslog.log("Config file not found, generating default");
Logger.syslog.log("Edit cfg.json to configure");
data = "{}";
}
else {
Logger.errlog.log("Config load failed");
Logger.errlog.log(err);
return;
}
}
try {
data = JSON.parse(data + "");
} catch(e) {
Logger.errlog.log("Config JSON is invalid: ");
Logger.errlog.log(e);
return;
}
for(var k in data)
cfg[k] = data[k];
if(cfg["enable-mail"]) {
cfg["nodemailer"] = nodemailer.createTransport(
cfg["mail-transport"],
cfg["mail-config"]
);
}
save(cfg, file);
Server.cfg = cfg;
callback();
});
}

228
config.template.yaml Normal file
View file

@ -0,0 +1,228 @@
# MySQL server details
# server: domain, IP or unix socket path of MySQL server. If a unix socket, it be like so `unix:/path/to/sock.sock`
# database: a MySQL database that the user specified has read/write access to
# user: username to authenticate as
# password: password for user
mysql:
server: 'mysql'
port: 3306
database: 'cytube3'
user: 'cytube3'
password: 'strong-password-here'
pool-size: 10
# Define IPs/ports to listen on
# Each entry MUST define ip and port (ip can be '' to bind all available addresses)
# Each entry should set http, https, and/or io to true to listen for the corresponding
# service on that port. http/io and https/io can be combined, but if http and https
# are both specified, only https will be bound to that port.
#
# If you don't specify a url, the url io.domain:port or https.domain:port will be assumed
# for non-ssl and ssl websockets, respectively. You can override this by specifying the
# url for a websocket listener.
listen:
# Default HTTP server - default interface, port 8080
- ip: ''
port: 8080
http: true
# Uncomment below to enable HTTPS/SSL websockets
# Note that you must also set https->enabled = true in the https definition
# - ip: ''
# port: 8443
# https: true
# io: true
# Default Socket.IO server - default interface, port 1337
- ip: ''
port: 1337
io: true
# Example of how to bind an extra port to HTTP and Socket.IO
# - ip: ''
# port: 8081
# http: true
# io: true
# url: 'http://my-other-thing.site.com:8081'
# HTTP server details
http:
# Even though you may specify multiple ports to listen on for HTTP above,
# one port must be specified as default for the purposes of generating
# links with the appropriate port
default-port: 8080
# Specifies the root domain for cookies. If you have multiple domains
# e.g. a.example.com and b.example.com, the root domain is example.com
root-domain: 'localhost'
# Specify alternate domains/hosts that are allowed to set the login cookie
# Leave out the http://
alt-domains:
- '127.0.0.1'
# Use express-minify to minify CSS and Javascript
minify: false
# Max-Age for caching. Value should be an integer in milliseconds or a string accepted by
# the `ms` module. Set to 0 to disable caching.
max-age: '7d'
# Set to false to disable gzip compression
gzip: true
# Customize the threshold byte size for applying gzip
gzip-threshold: 1024
# Secret used for signed cookies. Can be anything, but make it unique and hard to guess
cookie-secret: 'change-me'
index:
# Maximum number of channels to display on the index page public channel list
max-entries: 50
# Configure trusted proxy addresses to map X-Forwarded-For to the client IP.
# See also: https://github.com/jshttp/proxy-addr
trust-proxies: ['loopback']
# HTTPS server details
https:
enabled: false
# Even though you may specify multiple ports to listen on for HTTPS above,
# one port must be specified as default for the purposes of generating
# links with the appropriate port
default-port: 8443
domain: 'https://localhost'
keyfile: 'localhost.key'
passphrase: ''
certfile: 'localhost.cert'
cafile: ''
ciphers: 'HIGH:!DSS:!aNULL@STRENGTH'
# Page template values
# title goes in the upper left corner, description goes in a <meta> tag
html-template:
title: 'Sync'
description: 'Free, open source synchtube'
# Socket.IO server details
io:
# In most cases this will be the same as the http.domain.
# However, if your HTTP traffic is going through a proxy (e.g. cloudflare)
# you will want to set up a passthrough domain for socket.io.
# If the root of this domain is not the same as the root of your HTTP domain
# (or HTTPS if SSL is enabled), logins won't work.
domain: 'http://localhost'
# Even though you may specify multiple ports to listen on for HTTP above,
# one port must be specified as default for the purposes of generating
# links with the appropriate port
default-port: 1337
# limit the number of concurrent socket connections per IP address
ip-connection-limit: 10
cors:
# Additional origins to allow socket connections from (io.domain and
# https.domain are included implicitly).
allowed-origins: []
# YouTube v3 API key
# 1. Go to https://console.developers.google.com/, create a new "project" (or choose an existing one)
# 2. Make sure the YouTube Data v3 API is "enabled" for your project: https://console.developers.google.com/apis/library/youtube.googleapis.com
# 3. Go to "Credentials" on the sidebar of https://console.developers.google.com/, click "Create credentials" and choose type "API key"
# 4. Optionally restrict the key for security, or just copy the key.
# 5. Test your key (may take a few minutes to become active):
#
# $ export YOUTUBE_API_KEY="your key here"
# $ curl "https://www.googleapis.com/youtube/v3/search?key=$YOUTUBE_API_KEY&part=id&maxResults=1&q=test+video&type=video"
youtube-v3-key: ''
# Limit for the number of channels a user can register
max-channels-per-user: 5
# Limit for the number of accounts an IP address can register
max-accounts-per-ip: 5
# Minimum number of seconds between guest logins from the same IP
guest-login-delay: 60
# Maximum character length of a chat message, capped at 1000 characters
max-chat-message-length: 320
# Allows you to customize the path divider. The /r/ in http://localhost/r/yourchannel
# Acceptable characters are a-z A-Z 0-9 _ and -
channel-path: 'r'
# Allows you to blacklist certain channels. Users will be automatically kicked
# upon trying to join one.
channel-blacklist: []
# Minutes between saving channel state to disk
channel-save-interval: 5
# Configure periodic clearing of old alias data
aliases:
# Interval (in milliseconds) between subsequent runs of clearing
purge-interval: 3600000
# Maximum age of an alias (in milliseconds) - default 1 month
max-age: 2592000000
# Workaround for Vimeo blocking my domain
vimeo-workaround: false
# Regular expressions for defining reserved user and channel names and page titles
# The list of regular expressions will be joined with an OR, and compared without
# case sensitivity.
#
# Default: reserve any name containing "admin[istrator]" or "owner" as a word
# but only if it is separated by a dash or underscore (e.g. dadmin is not reserved
# but d-admin is)
reserved-names:
usernames:
- '^(.*?[-_])?admin(istrator)?([-_].*)?$'
- '^(.*?[-_])?owner([-_].*)?$'
channels:
- '^(.*?[-_])?admin(istrator)?([-_].*)?$'
- '^(.*?[-_])?owner([-_].*)?$'
pagetitles: []
# Provide a contact list for the /contact page
# Example:
# contacts:
# - name: 'my_name'
# title: 'administrator
# email: 'me@my.site'
contacts: []
playlist:
max-items: 4000
# How often (in seconds), mediaUpdate packets are broadcast to clients
update-interval: 5
# If set to true, when the ipThrottle and lastguestlogin rate limiters are cleared
# periodically, the garbage collector will be invoked immediately.
# The server must be invoked with node --expose-gc index.js for this to have any effect.
aggressive-gc: false
# If you have ffmpeg installed, you can query metadata from raw files, allowing
# server-synched raw file playback. This requires the following:
# * ffmpeg must be installed on the server
ffmpeg:
enabled: false
# Executable name for ffprobe if it is not "ffprobe". On Debian and Ubuntu (on which
# libav is used rather than ffmpeg proper), this is "avprobe"
ffprobe-exec: 'ffprobe'
link-domain-blacklist: []
# Drop root if started as root!!
setuid:
enabled: false
group: 'users'
user: 'user'
# how long to wait in ms before changing uid/gid
timeout: 15
# Allows for external services to access the system commandline
# Useful for setups where stdin isn't available such as when using PM2
service-socket:
enabled: false
socket: 'service.sock'
# Twitch Client ID for the data API (used for VOD lookups)
# https://github.com/justintv/Twitch-API/blob/master/authentication.md#developer-setup
twitch-client-id: null
poll:
max-options: 50
calendar-sync:
enabled: false
# 32-byte key encoded as base64. Prefer setting via env CALENDAR_SYNC_ENCRYPTION_KEY.
encryption-key: ''
google:
client-id: ''
client-secret: ''
# Must exactly match Google OAuth redirect URI
# Example: https://your-domain/api/v1/integrations/google/callback
redirect-uri: ''

View file

@ -1,17 +0,0 @@
const allowed = ["iframe", "object", "param", "embed"];
const tag_re = /<\s*\/?\s*([a-z]+)(\s*([a-z]+)\s*=\s*('[^']*'|"[^"]*"|[^"'>]*))*\s*>/ig;
function filter(str) {
str = str.replace(tag_re, function (match, tag) {
if(!~allowed.indexOf(tag.toLowerCase())) {
return match.replace("<", "&lt;").replace(">", "&gt;");
}
return match;
});
str = str.replace(/(\bon\w*\s*=\s*('[^']*'|"[^"]"|[^\s><]*))/ig, function () {
return "";
});
return str;
}
exports.filter = filter;

File diff suppressed because it is too large Load diff

35
docker-compose.yml Normal file
View file

@ -0,0 +1,35 @@
version: "3.7"
services:
sync:
build: .
depends_on:
mysql:
condition: service_healthy
ports:
- "8080:8080"
- "1337:1337"
- "8443:8443"
volumes:
- "./templates/head.pug:/app/templates/head.pug"
- "./favicon.ico:/app/www/favicon.ico"
- "./config.yaml:/app/config.yaml"
mysql:
image: docker.io/library/mariadb:latest
healthcheck:
test: ["CMD", "mariadb-admin","ping","-h","localhost", "-u", "root", "--password=sync"]
interval: 5s
timeout: 3s
retries: 100
environment:
- MARIADB_AUTO_UPGRADE=1
- MARIADB_ROOT_PASSWORD=sync
- MARIADB_DATABASE=cytube3
- MARIADB_USER=cytube3
- MARIADB_PASSWORD=strong-password-here
volumes:
# This will create and mount the mysql files in the same folder as the docker-compose.yml file.
# You can change this to be anywhere.
# This will provide data persistence to your MariaDB database.
- "./mysql:/var/lib/mysql"
# If you are using a reverse proxy please do not forget to add the network here as well.
# Refer to the Readme for more information regarding using a Reverse Proxy.

20
docs/account-mgmt.md Normal file
View file

@ -0,0 +1,20 @@
## Registering an Account ##
To register an account, click the Account dropdown on the top bar, then click Register. This option will only appear if you are not already logged in. When choosing a username, make sure it meets the following requirements:
* Must be 1-20 characters long
* May only contain the letters 'A' through 'Z' (upper or lowercase), the digits '0' through '9', hyphens '-', and underscores '_'
Be sure to pick a strong password that is not easy to guess and is not the same as your accounts on other websites. Entering an email address is optional, and will allow you to recover your account if you ever forget your password.
## Changing your Password or Email Address ##
On the top bar of the website, click Account, then "Change Password/Email". You must provide your current password in order to change either of these. You can leave the email address box blank if you wish to remove the email address from your account (note that this means you will no longer be able to receive password reset emails).
## Recovering an Account If You Lost the Password ##
From the login page, click "Forgot password?" You will be prompted to enter your username and email address. The email address must match the one associated with your account. If you have not added an email address to your account, you cannot reset your password automatically and will need to contact an administrator for help. Otherwise, you will receive an email with a link to reset your password.
## Account Profile ##
Each CyTube account can set a profile photo and short description. On the top bar, click Account, then Profile. You can then enter the URL of a profile image, and enter a short text blurb that will be displayed when users hover over your name in the chat username list. Note that this is publicly visible to anyone, so don't use a private photo or include private information in your description.

570
docs/bot-api.md Normal file
View file

@ -0,0 +1,570 @@
# Bot API
Bots connect to a channel in two ways:
- **WebSocket** (socket.io) — real-time events: chat, user list, playlist changes, media updates
- **REST** (`/api/v1/...`) — commands: queue/delete/shuffle/clear playlist, manage emotes, read/write settings, kick/ban users
Chat can only be sent and received over the WebSocket. The REST API has no chat endpoint.
---
## Ranks
| Name | Value |
|-----------|-------|
| Moderator | 2 |
| Admin | 3 |
| Owner | 4 |
| Creator | 5 |
A bot's rank is capped at the rank of the user who issued its token. Every REST endpoint that modifies state has a minimum rank requirement listed in its description.
---
## Issuing a token
Tokens are issued in the channel settings modal on the **Bots** tab. You need at least moderator rank (2) to see this tab.
Fill in a name (120 alphanumeric, `-`, `_` characters), choose a rank, and click **Issue Token**. The token is shown **once** — copy it immediately. It looks like:
```
cbt_a3f8e2...64 hex characters...
```
To revoke a token, click **Revoke** next to it in the table. Any live WebSocket connections using that token are disconnected immediately.
---
## WebSocket connection
The bot authenticates by passing the token in the socket.io `auth` object:
```js
const io = require('socket.io-client');
const socket = io('http://your-server:1337', {
auth: { token: process.env.BOT_TOKEN }
});
```
> **Port note:** socket.io runs on the port defined by `io.port` in your server config (default 1337), which is separate from the HTTP port used for the web UI and REST API.
### Join sequence
After connecting you must wait for the `login` event before emitting `joinChannel`, otherwise the server will ignore it:
```js
socket.once('login', () => {
socket.emit('joinChannel', { name: 'your-channel-name' });
});
```
### Sending chat
```js
socket.emit('chatMsg', { msg: 'Hello from a bot!' });
// Action message:
socket.emit('chatMsg', { msg: '/me waves' });
```
### Events the bot receives
| Event | Payload | Description |
|------------------|----------------------------------------------|-------------------------------------|
| `login` | `{ success, name, guest }` | Server accepted the connection |
| `chatMsg` | `{ username, msg, meta, time }` | A chat message was sent |
| `userlist` | `[{ name, rank, meta }, ...]` | Full user list on join |
| `addUser` | `{ name, rank, meta }` | A user joined the channel |
| `userLeave` | `{ name }` | A user left the channel |
| `setUserMeta` | `{ name, meta }` | A user's meta (AFK, muted) changed |
| `changeMedia` | `{ id, type, title, seconds, ... }` | A new video started playing |
| `playlist` | `[{ uid, media, queueby, temp }, ...]` | Full playlist on join |
| `queue` | `{ item, after }` | An item was added to the playlist |
| `delete` | `{ uid }` | A playlist item was removed |
| `errorMsg` | `{ msg }` | An error from the server |
| `kick` | `{ reason }` | The bot was kicked |
| `announcement` | `{ title, text }` | Server-wide announcement |
`meta.is_bot` is set to `true` in `chatMsg` and user list payloads for bots, which the web UI renders as a `[bot]` badge.
---
## REST API
### Base URL
```
http://your-server:8080/api/v1
```
All channel-scoped endpoints are under `/channels/:channel/`.
### Authentication
Every REST request must include the bot token as a Bearer token:
```
Authorization: Bearer cbt_...
```
The token is validated against the channel in the URL — a token issued for `#general` will be rejected for `/channels/gaming/...`.
### Error responses
All errors return JSON:
```json
{ "error": "Human readable message" }
```
Common status codes:
| Code | Meaning |
|------|-------------------------------------------------------|
| 400 | Bad request (missing/invalid fields) |
| 401 | Missing or invalid token |
| 403 | Token not authorized for this channel, or rank too low |
| 404 | Resource not found |
| 503 | Channel is not currently active (no users in it) |
---
### Playlist
Playlist endpoints require the channel to be active (at least one user present).
#### `GET /channels/:channel/playlist`
Returns the current playlist and which item is playing.
No minimum rank.
**Response:**
```json
{
"items": [
{
"uid": 1,
"id": "dQw4w9WgXcQ",
"type": "yt",
"title": "Rick Astley - Never Gonna Give You Up",
"seconds": 212,
"duration": "3:32",
"thumb": { "url": "..." },
"meta": {}
}
],
"currentIndex": 0,
"locked": false
}
```
`uid` is the server-assigned unique ID for the playlist slot. Use it for delete/jump operations.
#### `POST /channels/:channel/playlist`
Queue a new item. Minimum rank: **2 (Mod)**.
Returns `202 Accepted` immediately because media lookup is asynchronous. If the bot's permissions don't allow the add (e.g. playlist is locked and rank is too low), a `400` is returned synchronously.
**Body:**
```json
{ "id": "dQw4w9WgXcQ", "type": "yt", "pos": "end" }
```
| Field | Required | Values | Default |
|-------|----------|---------------------|---------|
| `id` | yes | media ID string | |
| `type`| yes | see media types below | |
| `pos` | no | `"end"` or `"next"` | `"end"` |
**Media types:**
| Type | Source |
|------|----------------------|
| `yt` | YouTube |
| `sc` | SoundCloud |
| `tw` | Twitch stream |
| `tc` | Twitch clip |
| `rt` | Dailymotion |
| `vm` | Vimeo |
| `dm` | Dailymotion |
| `gd` | Google Drive |
| `fi` | Direct file URL |
| `cu` | Custom embed (HTML) |
#### `DELETE /channels/:channel/playlist/:uid`
Remove a playlist item by uid. Minimum rank: **3 (Admin)**.
#### `PUT /channels/:channel/playlist/playing`
Skip to a specific item. Minimum rank: **2 (Mod)**.
**Body:**
```json
{ "uid": 3 }
```
#### `POST /channels/:channel/playlist/shuffle`
Shuffle the playlist. Minimum rank: **3 (Admin)**.
#### `POST /channels/:channel/playlist/clear`
Clear the entire playlist. Minimum rank: **3 (Admin)**.
---
### Emotes
Emote endpoints read and write directly to the database and work even when the channel is offline. If the channel happens to be active, changes are broadcast live to connected users.
#### `GET /channels/:channel/emotes`
Returns the full emote list. No minimum rank.
**Response:**
```json
[
{ "name": "KEKW", "image": "https://cdn.example.com/kekw.png", "source": "..." }
]
```
#### `POST /channels/:channel/emotes`
Add a new emote. Minimum rank: **4 (Owner)**.
**Body:**
```json
{ "name": "KEKW", "image": "https://cdn.example.com/kekw.png" }
```
Returns `409 Conflict` if the name is already taken.
#### `PUT /channels/:channel/emotes/:name`
Update an emote's image or rename it. Minimum rank: **4 (Owner)**.
**Body** (all fields optional, but at least one must differ):
```json
{ "image": "https://cdn.example.com/kekw-v2.png", "newName": "KEKWait" }
```
Omit `newName` to keep the existing name. Returns `409` if the new name is already taken.
#### `DELETE /channels/:channel/emotes/:name`
Delete an emote by name. Minimum rank: **4 (Owner)**.
---
### Users / Moderation
These endpoints require the channel to be active.
#### `GET /channels/:channel/users`
List currently connected users. No minimum rank.
**Response:**
```json
[
{ "name": "Alice", "rank": 3, "afk": false, "is_bot": false },
{ "name": "MyBot", "rank": 2, "afk": false, "is_bot": true }
]
```
#### `POST /channels/:channel/users/:name/kick`
Kick a user. Minimum rank: **2 (Mod)**. Cannot kick users with equal or higher rank.
**Body:**
```json
{ "reason": "Spamming" }
```
`reason` is optional, defaults to `"Kicked by bot"`.
#### `POST /channels/:channel/users/:name/ban`
Ban a user by name. Minimum rank: **3 (Admin)**. Cannot ban users with equal or higher rank.
**Body:**
```json
{ "reason": "Ban evasion" }
```
`reason` is optional, defaults to `"Banned by bot"`.
#### `DELETE /channels/:channel/users/:name/ban`
Remove a ban. Minimum rank: **3 (Admin)**.
#### `PUT /channels/:channel/users/:name/rank`
Change a user's channel rank. Minimum rank: **4 (Owner)**. Cannot assign a rank equal to or higher than your own, and cannot target users with equal or higher rank.
**Body:**
```json
{ "rank": 2 }
```
---
### Settings
Settings endpoints require the channel to be active. Minimum rank: **4 (Owner)** for both read and write.
#### `GET /channels/:channel/settings`
Returns current channel options.
**Response:**
```json
{
"pagetitle": "My Channel",
"allow_voteskip": true,
"voteskip_ratio": 0.5,
"maxlength": 0,
"afk_timeout": 600,
"password": "",
"show_public": false,
...
}
```
**Available keys:**
`allow_voteskip`, `allow_dupes`, `voteskip_ratio`, `maxlength`, `playlist_max_duration_per_user`, `afk_timeout`, `enable_link_regex`, `chat_antiflood`, `chat_antiflood_burst`, `chat_antiflood_sustained`, `new_user_chat_delay`, `new_user_chat_link_delay`, `pagetitle`, `password`, `externalcss`, `externaljs`, `show_public`, `torbanned`, `block_anonymous_users`, `allow_ascii_control`, `playlist_max_per_user`
#### `PUT /channels/:channel/settings`
Update one or more settings. Unknown keys are silently ignored.
**Body:**
```json
{ "pagetitle": "Now Playing: Chill Beats", "allow_voteskip": false }
```
---
### Shows
Show endpoints manage scheduled playlist runs. These endpoints support bot Bearer auth and session auth.
#### `GET /channels/:channel/shows`
List shows for the channel. Minimum rank: **2 (Mod)**.
#### `GET /channels/:channel/shows/:id`
Get a single show. Minimum rank: **2 (Mod)**.
#### `GET /channels/:channel/shows/public`
List only public-facing shows (`scheduled`, `running`, `paused`, `completed`). No auth required.
#### `POST /channels/:channel/shows/resolve-media`
Resolve up to 50 media entries to display-ready titles before saving a show. Minimum rank: **2 (Mod)**.
**Body:**
```json
{
"items": [
{ "type": "yt", "id": "dQw4w9WgXcQ" }
]
}
```
**Response:**
```json
{
"items": [
{ "type": "yt", "id": "dQw4w9WgXcQ", "title": "Rick Astley - Never Gonna Give You Up", "ok": true }
]
}
```
#### `POST /channels/:channel/shows`
Create a show. Minimum rank: **2 (Mod)**.
#### `PUT /channels/:channel/shows/:id`
Update a show. Minimum rank: **2 (Mod)**.
#### `DELETE /channels/:channel/shows/:id`
Delete a show. Minimum rank: **3 (Admin)**.
#### `POST /channels/:channel/shows/:id/action`
Run control action.
| Action | Minimum rank |
|------------|--------------|
| `pause` | 2 |
| `resume` | 2 |
| `schedule` | 2 |
| `run` | 3 |
| `cancel` | 3 |
**Create/Update body schema:**
```json
{
"name": "Friday Prime",
"notes": "Opening block",
"color": "#337AB7",
"scheduled_for": "2026-05-22T19:00:00.000Z",
"estimated_end_at": "2026-05-22T21:00:00.000Z",
"timezone": "America/New_York",
"recurrence": "weekly",
"fill_mode": "replace",
"conflict_mode": "force",
"start_playback": true,
"playlist": [
{ "type": "yt", "id": "dQw4w9WgXcQ", "pos": "end" }
],
"status": "scheduled"
}
```
**Field constraints:**
- `name`: required, 1-100 chars
- `notes`: optional string, trimmed and capped to 20,000 chars
- `color`: optional `#RRGGBB` hex color
- `timezone`: required IANA timezone string (example: `Europe/Berlin`, `America/New_York`)
- `scheduled_for`: required date string or unix timestamp (ms)
- `estimated_end_at`: optional date string/timestamp, must be later than `scheduled_for` when present
- `recurrence`: `none | daily | weekly`
- `fill_mode`: `append | replace`
- `conflict_mode`: `force | skip`
- `playlist`: non-empty array of media entries (`type`, `id`, optional `pos: next|end`)
- `status`: one of `draft | scheduled | paused | running | completed | failed | canceled` (`running` is accepted but normalized to `scheduled` on write)
**Action body schema:**
```json
{ "action": "run" }
```
**Show response shape** (`GET`, `POST`, `PUT`, and `POST /action`):
```json
{
"id": 42,
"channel_name": "my-channel",
"channel_id": 10,
"name": "Friday Prime",
"notes": "Opening block",
"notes_html": "<p>Opening block</p>",
"color": "#337AB7",
"playlist": [{ "type": "yt", "id": "dQw4w9WgXcQ", "pos": "end" }],
"timezone": "America/New_York",
"scheduled_for": 1779476400000,
"estimated_end_at": 1779483600000,
"next_run_at": 1779476400000,
"status": "scheduled",
"recurrence": "weekly",
"recurrence_meta": null,
"fill_mode": "replace",
"conflict_mode": "force",
"start_playback": true,
"run_count": 0,
"last_run_at": null,
"created_at": 1779400000000,
"updated_at": 1779400000000,
"created_by": "my-bot",
"updated_by": "my-bot",
"last_error": null
}
```
---
### Bot management
These endpoints use **session cookie auth** (the normal logged-in web session), not a bot token. They are intended for the channel settings UI.
#### `GET /channels/:channel/bots`
List all bots for the channel. Returns id, name, rank, creator, creation time, last connection time. Requires channel rank ≥ 2.
#### `POST /channels/:channel/bots`
Issue a new bot token. Requires channel rank ≥ 2. The returned `token` is shown **once** and not stored.
**Body:**
```json
{ "name": "MyBot", "rank": 2 }
```
**Response:**
```json
{ "id": 7, "name": "MyBot", "rank": 2, "token": "cbt_..." }
```
#### `DELETE /channels/:channel/bots/:id`
Revoke a bot token. Any live socket connections for that bot are disconnected immediately. Requires channel rank ≥ 2 and your rank must be ≥ the bot's rank.
---
## Demo bot
A working example with a TUI is in `examples/demo-bot/`.
```
cd examples/demo-bot
npm install
cp .env.example .env
# Edit .env: fill in BOT_TOKEN, CHANNEL, SERVER_URL, API_BASE
node bot.js
```
**.env fields:**
| Variable | Description | Default |
|--------------|---------------------------------------------------------|-----------------------------------|
| `BOT_TOKEN` | Bot token from the channel settings Bots tab | — |
| `CHANNEL` | Channel name (lowercase, no `#`) | — |
| `SERVER_URL` | socket.io URL (use the io.port, not the HTTP port) | `http://localhost:1337` |
| `API_BASE` | Base URL for REST requests | `http://localhost:8080/api/v1` |
**TUI keybindings:** Enter to send, PgUp/PgDn to scroll, Ctrl-C to quit.
**Bot commands** (prefix with `/` in the input box):
| Command | Description |
|--------------------------|----------------------------------------|
| `/help` | List commands |
| `/playlist` | Show playlist via REST |
| `/emotes` | List emotes via REST |
| `/settings` | Show channel settings via REST |
| `/users` | Show connected user list |
| `/add <type> <id>` | Queue media, e.g. `/add yt dQw4w9WgXcQ` |
| `/skip` | Skip to next item |
| `/clear` | Clear the playlist |
| `/kick <name> [reason]` | Kick a user |
| `/me <text>` | Send an action message |

67
docs/chat-filters.md Normal file
View file

@ -0,0 +1,67 @@
## Please do not use chat filters for emoticons! ##
CyTube has an emotes feature which is better-suited for adding emoticons.
Adding them as chat filters is more difficult to manage and uses more server
resources.
## Managing Chat Filters ##
You can access the Chat Filters editor by clicking on "Channel Settings" at the
top of the page, then the "Edit" dropdown, and selecting "Chat Filters".
### Adding a New Chat Filter ###
The first field allows you to enter a unique name for the filter. This can be
anything you like, but it must be unique among all filters on your channel.
The "Filter regex" field is where you input the [regular
expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp)
that you would like to match. Regular expressions allow you to build
sophisticated filters that can find and replace patterns rather than simple
words. If you simply want to filter a word, you can just use `\bword\b`, as
long as the word does not contain any of the special characters listed on the
linked regular expression guide. The leading and trailing `\b` ensure that you
only match the whole word "word", and do not match instances where it is nested
inside other words.
If you're looking for a way to test your regular expression, there are many free
tools available online, such as [this one](http://regexpal.com/).
The "Flags" field allows you to control certain aspects of matching. The "g"
flag specifies that replacement will be done "globally"-- it will replace all
instances of the regular expression instaed of just the first one. The "i" flag
makes matching case-insensitive, so that the capitalization of the message
doesn't matter. Flags can be combined by putting both of them in the box, e.g.
"gi".
The "Replacement" field is where you specify the text to be substituted for the
original messagse. This allows a limited subset of HTML tags to be used.
## Editing Filters ##
From the chat filter list, you can drag and drop filters to rearrange the order
in which they are executed. For each filter, there are two buttons. The left
button allows you to edit the filter, to update the regular expression, flags,
replacement, and whether or not the filter should be applied to links inside of
messages (this defaults to off). The red trash can button removes the filter.
## Export/Import ##
The export/import feature allows you to back up your filter list and restore it
later, or clone filters to a new channel. Clicking "Export filter list" will
populate the below textarea with a JSON encoded version of the filter list.
Copy this and save it somewhere safe. Later, you can paste this same text back
into the box and click "Import filter list" to overwrite your current filters
with the exported list.
## Notes ##
* By default, CyTube automatically replaces URLs in chat messages with
clickable links. You can disable this from the "Chat Settings" section
under the "General Settings" tab.
* By default, chat filters will not replace text inside of links, to prevent
them from being broken by the filter. You can override this by editing the
filter and checking the "Filter Links" box.
* Incoming messages have HTML special characters sanitized before messages are
filtered. You will have to account for this if you want to filter these
characters. For example, instead of matching `<`, you must match `&lt;`.

210
docs/custom-media.md Normal file
View file

@ -0,0 +1,210 @@
CyTube Custom Content Metadata
==============================
*Last updated: 2022-02-12*
## Purpose ##
CyTube currently supports adding custom audio/video content by allowing the user
to supply a direct URL to an audio/video file. The server uses `ffprobe` to
probe the file for various metadata, including the codec/container format and
the duration. This approach has a few disadvantages over the officially
supported media providers, namely:
* Since it accepts a single file, it is not possible to provide multiple
source URLs with varying formats or bitrates to allow viewers to select the
best source for their computer.
- It also means it is not possible to provide text tracks for subtitles or
closed captioning, or to provide image URLs for thumbnails/previews.
* Probing the file with `ffprobe` is slow, especially if the content is hosted
in a far away network location, which at best is inconvenient and at worst
results in timeouts and inability to add the content.
* Parsing the `ffprobe` output is inexact, and may sometimes result in
detecting the wrong format, or failing to detect the title.
This document specifies a new supported media provider which allows users to
provide a JSON manifest specifying the metadata for custom content in a way that
avoids the above issues and is more flexible for extension.
## Custom Manifest URLs ##
Custom media manifests are added to CyTube by adding a link to a public URL
hosting the JSON metadata manifest. Pasting the JSON directly into CyTube is
not supported. Valid JSON manifests must:
* Have a URL path ending with the file extension `.json` (not counting
querystring parameters)
* Be served with the `Content-Type` header set to `application/json`
* Be retrievable at any time while the item is on the playlist (CyTube may
re-request the metadata for an item already on the playlist to revalidate)
* Respond to valid requests with a 200 OK HTTP response code (redirects are
not supported)
* Respond within 10 seconds
* Not exceed 100 KiB in size
## Manifest Format ##
To add custom content, the user provides a JSON object with the following keys:
* `title`: A nonempty string specifying the title of the content. For legacy
reasons, CyTube currently truncates this to 100 UTF-8 characters.
* `duration`: A non-negative, finite number specifying the duration, in
seconds, of the content. This is what the server will use for timing
purposes. Decimals are allowed, but CyTube's timer truncates the value as
an integer number of seconds, so including fractional seconds lends no
advantage.
* `live`: An optional boolean (default: `false`) indicating whether the
content is live or pre-recorded. For live content, the `duration` is
ignored, and the server won't advance the playlist automatically.
* `thumbnail`: An optional string specifying a URL for a thumbnail image of
the content. CyTube currently does not support displaying thumbnails in the
playlist, but this functionality may be offered in the future.
* `sources`: A nonempty list of playable sources for the content. The format
is described below.
* `audioTracks`: An optional list of audio tracks for using demuxed audio
and providing multiple audio selections. The format is described below.
* `textTracks`: An optional list of text tracks for subtitles or closed
captioning. The format is described below.
### Source Format ###
Each source entry is a JSON object with the following keys:
* `url`: A valid URL that browsers can use to retrieve the content. The URL
must resolve to a publicly-routed IP address, and must the `https:` scheme.
* `contentType`: A string representing the MIME type of the content at `url`.
A list of acceptable MIME types is provided below.
* `quality`: A number representing the quality level of the source. The
supported quality levels are `240`, `360`, `480`, `540`, `720`, `1080`,
`1440`, and `2160`. This may be extended in the future.
* `bitrate`: An optional number indicating the bitrate (in Kbps) of the
content. It must be a positive, finite number if provided. The bitrate is
not currently used by CyTube, but may be used by extensions or custom
scripts to determine whether this source is feasible to play on the viewer's
internet connection.
#### Acceptable MIME Types ####
The following MIME types are accepted for the `contentType` field:
* `video/mp4`
* `video/webm`
* `video/ogg`
* `application/x-mpegURL` (HLS streams)
- HLS is only tested with livestreams. VODs are accepted, but I do not test
this functionality.
* `application/dash+xml` (DASH streams)
- Support for DASH is experimental
* ~~`rtmp/flv`~~
- In light of Adobe phasing out support for Flash, and many browsers
already dropping support, RTMP is not supported by this feature.
RTMP streams are only supported through the existing `rt:` media
type.
* `audio/aac`
* `audio/mp4`
* `audio/mpeg`
* `audio/ogg`
Other audio or video formats, such as AVI, MKV, and FLAC, are not supported due
to lack of common support across browsers for playing these formats. For more
information, refer to
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility).
### Audio Track Format ###
Each audio track entry is a JSON object with the following keys:
* `label`: A label for the audio track. This is displayed in the menu for the
viewer to select a text track.
* `language`: A two or three letter IETF BCP 47 subtype code indicating the
language of the audio track.
* `url`: A valid URL that browsers can use to retrieve the track. The URL
must resolve to a publicly-routed IP address, and must use the `https:` scheme.
* `contentType`: A string representing the MIME type of the track at `url`.
Any type starting with `audio` from the list above is acceptable. However
the usage of audio/aac is known to cause audio syncrhonization problems
for some users. It is recommended to use an m4a file to wrap aac streams.
**Important note regarding audio tracks:**
Because of browsers trying to be too smart for their own good, you should
include a silent audio stream in the video sources when using separate audio
tracks. If you do not, the browser will automatically pause the video whenever
the browser detects the page as not visible. There is no way to instruct it to
not do so. You can readily accomplish the inclusion of a silent audio track
with ffmpeg using the anullsrc filter like so:
`ffmpeg -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=48000 -i input.mp4 -c:v copy -c:a aac -shortest output.mp4`
It is recommended to match the sample rate and codec you intend to use in your
audioTracks in your silent track.
### Text Track Format ###
Each text track entry is a JSON object with the following keys:
* `url`: A valid URL that browsers can use to retrieve the track. The URL
must resolve to a publicly-routed IP address, and must the `https:` scheme.
* `contentType`: A string representing the MIME type of the track at `url`.
The only currently supported MIME type is
[`text/vtt`](https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API).
* `name`: A name for the text track. This is displayed in the menu for the
viewer to select a text track.
* `default`: Enable track by default. Optional boolean attribute to enable
a subtitle track to the user by default.
**Important note regarding text tracks and CORS:**
By default, browsers block requests for WebVTT tracks hosted on different
domains than the current page. In order for text tracks to work cross-origin,
the `Access-Control-Allow-Origin` header needs to be set by the remote server
when serving the VTT file. See
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin)
for more information about setting this header.
## Example ##
{
"title": "Test Video",
"duration": 10,
"live": false,
"thumbnail": "https://example.com/thumb.jpg",
"sources": [
{
"url": "https://example.com/video.mp4",
"contentType": "video/mp4",
"quality": 1080,
"bitrate": 5000
}
],
"textTracks": [
{
"url": "https://example.com/subtitles.vtt",
"contentType": "text/vtt",
"name": "English Subtitles",
"default": true
}
]
}
## Permissions ##
The permission node to allow users to add custom content is the same as the
permission node for the existing raw file support. Custom content is considered
as an extension of the existing feature.
## Unsupported/Undefined Behavior ##
The behavior under any the following circumstances is not defined by this
specification, and any technical support in these cases is voided. This list is
non-exhaustive.
* Source URLs or text track URLs are hosted on a third-party website that does
not have knowledge of its content being played on CyTube.
* The webserver hosting the source or text track URLs serves a different MIME
type than the one specified in the manifest.
* The webserver hosting the source or text track URLs serves a file that does
not match the MIME type specified in the `Content-Type` HTTP header returned
to the browser.
* The manifest includes source URLs or text track URLs with expiration times,
session IDs, etc. in the URL querystring.
* The manifest provides source URLs with non-silent audio as well as a list
of audioTracks.

View file

@ -0,0 +1,24 @@
# Google Drive Userscript Setup
In response to increasing difficulty and complexity of maintaining Google Drive
support, the native player is being phased out in favor of requiring a
userscript to allow each client to fetch the video stream links for themselves.
Users will be prompted with a link to `/google_drive_userscript`, which explains
the situation and instructs how to install the userscript.
As a server admin, you must generate the userscript from the template by using
the following command:
```sh
npm run generate-userscript <site name> <url> [<url>...]
```
The first argument is the site name as it will appear in the userscript title.
The remaining arguments are the URL patterns on which the script will run. For
example, for cytu.be I use:
```sh
npm run generate-userscript CyTube http://cytu.be/r/* https://cytu.be/r/*
```
This will generate `www/js/cytube-google-drive.user.js`. If you've changed the channel path, be sure to take that into account.

View file

@ -0,0 +1,24 @@
Adding subtitles to Google Drive
================================
1. Upload your video to Google Drive
2. Right click the video in Google Drive and click Manage caption tracks
3. Click Add new captions or transcripts
4. Upload a supported subtitle file
* I have verified that Google Drive will accept .srt and .vtt subtitles. It
might accept others as well, but I have not tested them.
Once you have uploaded your subtitles, they should be available the next time
the video is refreshed by CyTube (either restart it or delete the playlist item
and add it again). On the video you should see a speech bubble icon in the
controls, which will pop up a menu of available subtitle tracks.
## Limitations ##
* Google Drive converts the subtitles you upload into a custom format which
loses some information from the original captions. For example, annotations
for who is speaking are not preserved.
* As far as I know, Google Drive is not able to automatically detect when
subtitle tracks are embedded within the video file. You must upload the
subtitles separately (there are plenty of tools to extract
captions/subtitles from MKV and MP4 files).

15
docs/index.md Normal file
View file

@ -0,0 +1,15 @@
# User Guide #
This user guide is a work in progress rewrite of the old user guide. If you notice something is missing, it probably hasn't been ported over from [here](https://github.com/calzoneman/sync/wiki/CyTube-3.0-User-Guide) yet.
## I want to know more about... ##
* [Registering and managing my user account](account-mgmt.md)
* [Available user preferences](user-settings.md)
* [Adding subtitles to Google Drive videos](google-drive-subtitles.md)
* [Managing chat filters](chat-filters.md)
## I need help! ##
1. Please read the [FAQ](https://github.com/calzoneman/sync/wiki/Frequently-Asked-Questions) and check whether that answers your question.
2. If not, you can contact someone for help. IRC support is provided on `irc.esper.net #cytube` ([webchat](https://webchat.esper.net/?channels=cytube) available) for https://cytu.be and general questions about using the software. If nobody is available on IRC, or you want to speak privately, email one of the contacts on https://cytu.be/contact.

View file

@ -0,0 +1,29 @@
Restricting New Accounts from Chat
==================================
With the rising availability and popularity of VPNs and proxies, dedicated
trolls may often come back again and again with a new proxy after being IP
banned and continue spamming. In order to combat this, a new feature has been
added to make it more difficult to rejoin quickly and continue spamming.
Channel moderators now have the ability to configure 2 different settings:
* How long an account must be active before the user can send any chat message
* How long an account must be active before the user can send a chat message
containing a link
This limit applies to both chat messages sent to the channel as well as private
messages. Both of these settings can be configured from the Channel Settings
menu at the top of the page, under the General Settings tab. By default,
accounts must be at least 10 minutes old to chat, and 1 hour old to send links
in chat. Setting either restriction to 0 will disable that restriction.
The age of an account is determined as follows:
* If the user is logged in as a registered account, the registration time of
the account is used.
* Otherwise, the timestamp of the session cookie is used.
The session cookie is set whenever a user first joins a channel, and is reset
whenever the user's IP address changes. Different browsers will have different
session cookies.

59
docs/raw-videos.md Normal file
View file

@ -0,0 +1,59 @@
# Raw Videos / Audio #
Want to host your own video/audio files for use on CyTube? For servers with the
ffprobe module enabled, CyTube supports this! However, in order to provide a
consistent experience, there are limitations.
## Hosting the File ##
CyTube requires a direct link to the file in order to query it for metadata such
as duration and encoding. The website where you host the file needs to be able
to serve the video directly (rather than embedding it in a flash
player/iframe/etc.). It also needs to serve the correct MIME type for the video
in the `Content-Type` HTTP header, e.g. `video/mp4`.
I don't recommend hosting videos on Dropbox-type services, as they aren't built
to distribute video to many users at a time and often have strict bandwidth
limits. File hosting sites such as Putlocker also cause problems due to being
unable to serve the file directly, or due to binding the link to the IP address
of the user who retrieved it. For best results when using raw video, host the
video yourself on a VPS or dedicated server with plenty of bandwidth.
Note that CyTube only queries the file for metadata, it does not proxy it for
users! Every user watching the video will be downloading it individually.
## Encoding the Video ##
Current internet browsers are very limited in what codecs they can play
natively. Accordingly, CyTube only supports a few codecs:
**Video**
* MP4 (AV1)
* MP4 (H.264)
* WebM (AV1)
* WebM (VP8)
* WebM (VP9)
* Ogg/Theora
**Audio**
* MP3
* Ogg/Vorbis
If your video is in some other format (such as MKV or AVI), then it will need to
be re-encoded. There are plenty of free programs available to re-encode video
files, such as [ffmpeg](http://ffmpeg.org/) and
[handbrake](http://handbrake.fr/).
For best results, encode as an MP4 using H.264. This is natively supported by
many browsers, and can also be played using a fallback flash player for older
browsers that don't support it natively. Always encode with the
[faststart](https://trac.ffmpeg.org/wiki/Encode/H.264#faststartforwebvideo)
flag.
### Subtitles ###
Unfortunately, soft-subtitles are not supported right now. This is something
that may be supported in the future, but currently if you need subtitles, they
will have to be hardsubbed onto the video itself.

57
docs/socketconfig.md Normal file
View file

@ -0,0 +1,57 @@
Socket.IO Client Configuration
==============================
As of 2015-10-25, the legacy `/sioconfig` JavaScript for retrieving connection
information is being deprecated in favor of a new API. The purpose of this
change is to allow partitioning channels across multiple servers in order to
better handle increasing traffic.
To get the socket.io configuration for the server hosting a particular channel,
make a `GET` request to `/socketconfig/<channel name>.json`. The response will
be a JSON object containing a list of acceptable servers to connect to, or an
error message.
Examples:
```
GET /socketconfig/test.json
200 OK
{
"servers": [
{
"url": "https://localhost:8443",
"secure": true
},
{
"url": "http://localhost:1337",
"secure": false
},
{
"url": "https://local6:8443",
"secure": true,
"ipv6": true
},
{
"url": "http://local6:1337",
"secure": false,
"ipv6": true
}
]
}
GET /socketconfig/$invalid$.json
404 Not Found
{
"error": "Channel \"$invalid$\" does not exist."
}
```
Each entry in the `servers` array has `"secure":true` if the connection is
secured with TLS, otherwise it it is false. An entry with `"ipv6":true`
indicates that the server is listening on the IPv6 protocol.
You can pick any URL to connect socket.io to in order to join the specified
channel. I recommend picking one with `"secure":true`, only choosing an
insecure connection if implementing a TLS connection is infeasible.

55
docs/user-settings.md Normal file
View file

@ -0,0 +1,55 @@
# User Preferences #
From any CyTube channel, you can click the Options link at the top of the page to open a dialog where you can change your personal preferences. This page explains each of the available options.
## General ##
General interface preferences.
Setting | Description
--------|------------
Theme | Choose from different colorschemes for the website.
Layout | Choose from different layouts for elements on the page. Fluid layouts will expand to fill the entire window, while compact layouts will remain the same size. "Synchtube" layout is the same as the default layout, but mirrored.
Ignore Channel CSS | Don't load custom stylesheets for each channel. Requires a refresh to take effect.
Ignore Channel JavaScript | Don't load custom scripts for each channel. The Script Access tab allows you to manage your preferences on a per-channel basis, but if this setting is checked, scripts will be globally disallowed and you will not be prompted to accept them when joining a channel.
## Playback ##
Preferences for video playback and the playlist.
Setting | Description
--------|------------
Synchronize video playback | By default, CyTube attempts to synchronize the video so that everyone is watching at the same time. Some users with poor internet connections may wish to disable this in order to prevent excessive buffering due to constantly seeking forward.
Synch threshold | The number of seconds your video is allowed to be ahead/behind before it is forcibly seeked to the correct position. Should be set to at least 2 seconds to avoid buffering problems and choppy playback.
Remove the video player | Automatically remove the video player on page load. Equivalent to manually clicking Layout->Remove Video every time you load a channel.
Hide playlist buttons by default | Hides the control buttons from each video in the playlist, so that only the title is displayed. The control buttons can be shown by right clicking the video item in the playlist.
Old style playlist buttons | Legacy feature introduced in CyTube 2.0 for those who preferred the old 1.0-style video control buttons.
Quality Preference | Sets the preferred quality for player types that support quality selection (currently, this is YouTube, Vimeo, Dailymotion, Google Drive, and Google+). If your preferred quality is not available, the next lowest quality will be used.
## Chat ##
Preferences for the integrated chatroom.
Setting | Description
--------|------------
Show timestamps in chat | When enabled, a timestamp is prepended to each chat message. For example, `[09:45:10] message here`.
Sort userlist by rank | Controls whether the username list is sorted alphabetically and by rank, or just alphabetically.
Sort AFKers to bottom | When enabled, usernames of AFK users will be sorted to the bottom of the username list.
Blink page title on new messages | Controls the conditions under which the tab title blinks between the channel title and `*Chat*` when a new message arrives.
Notification sound on new messages | Controls the conditions under which a notification sound is played when a new message arrives.
Add a send button to chat | Adds a clickable button to send chat messages. Only really useful for virtual keyboards that lack a dedicated Enter key.
Disable chat emotes | Disables the automatic conversion of channel-defined emote codes to inline images.
## Script Access ##
Manage your preferences for allowing or denying custom scripts for channels you've visited. A channel will only appear here if you checked "Remember my preference" when allowing or denying a channel script. You can toggle the preference between "Allow" and "Deny", or click "Clear Preference" to remove the saved preference, so that you will be asked every time you join the channel.
## Moderator ##
Settings that only apply to channel moderators.
Setting | Description
--------|------------
Show name color | Colors your username in chat (the same color as in the username list). This setting is also controlled by the small button labeled "M" in the upper right corner of chat.
Show join messages | Display a message every time a user logs in to the chat.
Show shadowmuted messages | Show chat messages from shadowmuted users. These messages will appear ~~struck through~~, and only moderators with this setting enabled will see them.

View file

@ -0,0 +1,11 @@
# Issue a bot token in the channel settings modal (Bots tab), then paste it here.
BOT_TOKEN=cbt_your_token_here
# The channel this token was issued for.
CHANNEL=yourchannel
# Base URL of the CyTube server (no trailing slash).
SERVER_URL=http://localhost:1337
# REST API base (usually SERVER_URL + /api/v1).
API_BASE=http://localhost:8080/api/v1

594
examples/demo-bot/bot.js Normal file
View file

@ -0,0 +1,594 @@
#!/usr/bin/env node
/**
* CyTube Sync Demo Bot
*
* Shows the two halves of the bot API:
* WebSocket (socket.io-client) real-time events: chat, user list, playlist
* REST API (fetch) queries and commands: playlist, emotes, settings, moderation
*
* Setup:
* cp .env.example .env # fill in BOT_TOKEN and CHANNEL
* npm install
* node bot.js
*
* TUI keybindings:
* Enter send message or run command
* PgUp/PgDn scroll chat
* Ctrl-C quit
*
* Commands (prefix with /):
* /help list commands
* /playlist show current playlist via REST
* /emotes list emotes via REST
* /settings show channel settings via REST
* /users dump user list from in-memory state
* /add <type> <id> add media e.g. /add yt dQw4w9WgXcQ
* /skip skip to next playlist item
* /clear clear the playlist
* /kick <name> [reason] kick a user
* /me <text> send an action (/me) message
*/
'use strict';
require('dotenv').config();
const io = require('socket.io-client');
const fetch = require('node-fetch');
const blessed = require('blessed');
// ── Config ────────────────────────────────────────────────────────────────────
const {
BOT_TOKEN,
CHANNEL,
SERVER_URL = 'http://localhost:8080',
API_BASE = 'http://localhost:8080/api/v1',
} = process.env;
if (!BOT_TOKEN || !CHANNEL) {
console.error('BOT_TOKEN and CHANNEL must be set. Copy .env.example to .env and fill it in.');
process.exit(1);
}
if (!BOT_TOKEN.startsWith('cbt_')) {
console.error('BOT_TOKEN does not look right — it should start with cbt_');
process.exit(1);
}
// ── REST helpers ──────────────────────────────────────────────────────────────
async function apiRequest(method, path, body) {
const url = `${API_BASE}/channels/${CHANNEL}${path}`;
const opts = {
method,
headers: {
'Authorization': `Bearer ${BOT_TOKEN}`,
'Content-Type': 'application/json',
},
};
if (body !== undefined) opts.body = JSON.stringify(body);
const res = await fetch(url, opts);
const json = await res.json().catch(() => null);
if (!res.ok) {
throw new Error((json && json.error) || `HTTP ${res.status}`);
}
return json;
}
const api = {
get: (path) => apiRequest('GET', path),
post: (path, body) => apiRequest('POST', path, body),
put: (path, body) => apiRequest('PUT', path, body),
delete: (path) => apiRequest('DELETE', path),
};
// ── Utilities ─────────────────────────────────────────────────────────────────
function stripHtml(html) {
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
}
// Escape blessed tag syntax so server content can't inject markup.
function escBless(str) {
return String(str).replace(/\{/g, '\\{');
}
function hhmm(secs) {
return `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, '0')}`;
}
function timestamp() {
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
// ── In-memory state ───────────────────────────────────────────────────────────
const state = {
users: [], // userlist entries from server
nowPlaying: null, // current changeMedia payload
connected: false,
inChannel: false,
};
// ── TUI ───────────────────────────────────────────────────────────────────────
const screen = blessed.screen({
smartCSR: true,
title: `CyTube Bot :: #${CHANNEL}`,
fullUnicode: true,
forceUnicode: true,
});
// Top bar — connection status + now playing
const statusBar = blessed.box({
top: 0,
left: 0,
width: '100%',
height: 1,
tags: true,
style: { bg: 'blue', fg: 'white', bold: true },
content: ` CyTube Bot #${CHANNEL} Connecting...`,
});
// Main chat log (left, scrollable)
const chatLog = blessed.log({
top: 1,
left: 0,
width: '70%',
height: '100%-4',
border: { type: 'line' },
label: ' Chat ',
scrollable: true,
alwaysScroll: true,
scrollbar: { ch: ' ', style: { bg: 'cyan' } },
mouse: true,
tags: true,
wrap: true,
style: { border: { fg: 'cyan' }, label: { fg: 'cyan', bold: true } },
});
// User list sidebar (right)
const userList = blessed.list({
top: 1,
right: 0,
width: '30%',
height: '100%-4',
border: { type: 'line' },
label: ' Users ',
scrollable: true,
mouse: true,
tags: true,
style: {
border: { fg: 'cyan' },
label: { fg: 'cyan', bold: true },
item: { fg: 'white' },
selected: { fg: 'white' },
},
});
// Input box (bottom)
const inputBox = blessed.textbox({
bottom: 0,
left: 0,
width: '100%',
height: 3,
border: { type: 'line' },
label: ' Message — /help for commands — Ctrl-C to quit ',
inputOnFocus: true,
style: { border: { fg: 'green' }, label: { fg: 'green' } },
});
screen.append(statusBar);
screen.append(chatLog);
screen.append(userList);
screen.append(inputBox);
screen.key(['C-c'], () => process.exit(0));
// ── TUI helpers ───────────────────────────────────────────────────────────────
function setStatus(msg) {
statusBar.setContent(` CyTube Bot #${CHANNEL} ${msg}`);
screen.render();
}
function chat(line) {
chatLog.log(line);
screen.render();
}
function info(line) {
chat(`{cyan-fg}${line}{/cyan-fg}`);
}
function warn(line) {
chat(`{yellow-fg}${line}{/yellow-fg}`);
}
function fail(line) {
chat(`{red-fg}✗ ${line}{/red-fg}`);
}
function rebuildUserList() {
const items = state.users.map(u => {
const isBot = u.meta && u.meta.is_bot;
const afk = u.meta && u.meta.afk;
let rankStr;
if (u.rank >= 5) rankStr = '{red-fg}[creator]{/red-fg}';
else if (u.rank >= 4) rankStr = '{yellow-fg}[owner]{/yellow-fg}';
else if (u.rank >= 3) rankStr = '{magenta-fg}[admin]{/magenta-fg}';
else if (u.rank >= 2) rankStr = '{green-fg}[mod]{/green-fg}';
else rankStr = '';
const botTag = isBot ? ' {blue-fg}[bot]{/blue-fg}' : '';
const afkTag = afk ? ' {grey-fg}[afk]{/grey-fg}' : '';
return `${escBless(u.name)} ${rankStr}${botTag}${afkTag}`;
});
userList.setLabel(` Users (${items.length}) `);
userList.setItems(items);
screen.render();
}
// ── Socket.IO ─────────────────────────────────────────────────────────────────
const socket = io(SERVER_URL, {
// This is how the bot authenticates — token goes in socket.handshake.auth.token
auth: { token: BOT_TOKEN },
reconnection: true,
reconnectionDelay: 3000,
reconnectionAttempts: Infinity,
});
socket.on('connect', () => {
state.connected = true;
setStatus('Authenticating...');
});
socket.on('disconnect', (reason) => {
state.connected = false;
state.inChannel = false;
state.users = [];
rebuildUserList();
warn(`Disconnected: ${reason}`);
setStatus(`Disconnected — reconnecting...`);
});
socket.on('connect_error', (err) => {
fail(`Connection error: ${err.message}`);
});
// Server confirms authentication and sends the bot's display name + rank.
// We wait for this before joining the channel to avoid any ordering issues.
socket.on('login', (data) => {
if (data.success) {
info(`Authenticated as: ${escBless(data.name)}`);
socket.emit('joinChannel', { name: CHANNEL });
} else {
fail(`Authentication failed: ${escBless(data.error || 'unknown')}`);
}
});
socket.on('errorMsg', (data) => {
fail(escBless(data.msg || 'Server error'));
});
socket.on('kick', (data) => {
fail(`Kicked: ${escBless(data.reason || '')}`);
setStatus('Kicked from channel');
});
// ── Channel events ────────────────────────────────────────────────────────────
// Channel options (title, password, etc.)
socket.on('channelOpts', (opts) => {
const title = opts.pagetitle || CHANNEL;
const playing = state.nowPlaying ? `${escBless(state.nowPlaying.title)}` : '';
setStatus(`{bold}${escBless(title)}{/bold}${playing}`);
});
// ── User list events ──────────────────────────────────────────────────────────
// Full user list, sent on join and refresh
socket.on('userlist', (users) => {
state.users = users;
state.inChannel = true;
rebuildUserList();
info(`Joined #${CHANNEL}${users.length} user(s) present`);
});
// New user joined
socket.on('addUser', (user) => {
state.users = state.users.filter(u => u.name !== user.name);
state.users.push(user);
rebuildUserList();
chat(`{green-fg}→ ${escBless(user.name)} joined{/green-fg}`);
});
// User left
socket.on('userLeave', (data) => {
state.users = state.users.filter(u => u.name !== data.name);
rebuildUserList();
chat(`{red-fg}← ${escBless(data.name)} left{/red-fg}`);
});
// Rank changed for a user
socket.on('setUserRank', (data) => {
const user = state.users.find(u => u.name === data.name);
if (user) user.rank = data.rank;
rebuildUserList();
});
// AFK / mute state changed
socket.on('setUserMeta', (data) => {
const user = state.users.find(u => u.name === data.name);
if (user) Object.assign(user.meta, data.meta);
rebuildUserList();
});
// ── Chat events ───────────────────────────────────────────────────────────────
socket.on('chatMsg', (data) => {
const time = timestamp();
const name = escBless(data.username || '?');
const msg = escBless(stripHtml(data.msg || ''));
const isAction = data.meta && data.meta.addClass === 'action';
// is_bot is set by the server in the message meta; fall back to checking
// the local user list in case the message arrives before the userlist does.
const senderInList = state.users.find(u => u.name === data.username);
const isBot = (data.meta && data.meta.is_bot) ||
(senderInList && senderInList.meta && senderInList.meta.is_bot);
const botTag = isBot ? ' {blue-fg}[bot]{/blue-fg}' : '';
if (isAction) {
chat(`{grey-fg}[${time}]{/grey-fg}${botTag} {italic}* ${name} ${msg}{/italic}`);
} else {
chat(`{grey-fg}[${time}]{/grey-fg} {bold}${name}{/bold}${botTag}: ${msg}`);
}
});
// Private messages
socket.on('pm', (data) => {
const name = escBless(data.username || '?');
const msg = escBless(stripHtml(data.msg || ''));
chat(`{magenta-fg}[PM ← ${name}]{/magenta-fg} ${msg}`);
});
// Chat cleared by a moderator
socket.on('clearchat', () => {
chatLog.setContent('');
info('Chat was cleared by a moderator.');
screen.render();
});
// ── Playlist events ───────────────────────────────────────────────────────────
// Full playlist on join
socket.on('playlist', (items) => {
if (items.length > 0) {
info(`Playlist loaded: ${items.length} item(s)`);
}
});
// New item added to playlist by someone
socket.on('queue', (data) => {
const item = data.item;
if (item && item.media) {
info(`Queued: [${item.media.type}] ${escBless(item.media.title)}`);
}
});
// Currently playing changed
socket.on('changeMedia', (media) => {
state.nowPlaying = media;
const dur = media.seconds ? ` (${hhmm(media.seconds)})` : '';
const title = escBless(media.title || media.id);
setStatus(`${title}${dur}`);
info(`Now playing: {bold}${title}{/bold}${dur}`);
});
// ── Emote events ──────────────────────────────────────────────────────────────
socket.on('updateEmote', (emote) => {
info(`Emote updated: :${escBless(emote.name)}:`);
});
socket.on('removeEmote', (data) => {
info(`Emote removed: :${escBless(data.name)}:`);
});
// ── Commands ──────────────────────────────────────────────────────────────────
const COMMANDS = {
help() {
info('─── Commands ───────────────────────────────────────────────────');
info(' /playlist fetch and show the playlist');
info(' /emotes list emote names');
info(' /settings show channel settings');
info(' /users dump in-memory user list');
info(' /add <type> <id> add media e.g. /add yt dQw4w9WgXcQ');
info(' /skip skip to next playlist item');
info(' /clear clear the playlist');
info(' /kick <name> [reason] kick a user from the channel');
info(' /me <text> send an action message');
info(' <anything else> send as a chat message');
info('───────────────────────────────────────────────────────────────');
},
async playlist() {
try {
const data = await api.get('/playlist');
info(`─── Playlist (${data.items.length} item${data.items.length !== 1 ? 's' : ''}) ──`);
if (data.items.length === 0) {
info(' (empty)');
} else {
data.items.forEach((item, i) => {
const dur = item.seconds ? hhmm(item.seconds) : '?:??';
const marker = i === data.currentIndex ? '{yellow-fg}▶{/yellow-fg}' : ' ';
info(` ${marker} [${item.type}] ${escBless(item.title)} (${dur}) uid:${item.uid}`);
});
}
if (data.locked) warn(' Playlist is locked');
} catch (e) {
fail(`playlist: ${e.message}`);
}
},
async emotes() {
try {
const emotes = await api.get('/emotes');
info(`─── Emotes (${emotes.length}) ──`);
if (emotes.length === 0) {
info(' (none)');
} else {
// Print names in rows of 8
for (let i = 0; i < emotes.length; i += 8) {
info(' ' + emotes.slice(i, i + 8).map(e => `:${escBless(e.name)}:`).join(' '));
}
}
} catch (e) {
fail(`emotes: ${e.message}`);
}
},
async settings() {
try {
const s = await api.get('/settings');
info('─── Channel Settings ──');
for (const [k, v] of Object.entries(s)) {
if (v !== null && v !== '' && v !== false) {
info(` ${k}: ${escBless(String(v))}`);
}
}
} catch (e) {
fail(`settings: ${e.message}`);
}
},
users() {
info(`─── Users (${state.users.length}) ──`);
state.users.forEach(u => {
const rank = u.rank >= 5 ? 'creator' : u.rank >= 4 ? 'owner' : u.rank >= 3 ? 'admin' : u.rank >= 2 ? 'mod' : 'user';
const isBot = u.meta && u.meta.is_bot ? ' [bot]' : '';
info(` ${escBless(u.name)} (${rank} rank:${u.rank})${isBot}`);
});
},
async add(args) {
const parts = args.trim().split(/\s+/);
if (parts.length < 2) {
fail('Usage: /add <type> <id> e.g. /add yt dQw4w9WgXcQ');
return;
}
const [type, id] = parts;
try {
await api.post('/playlist', { type, id, pos: 'end' });
info(`Added [${type}] ${escBless(id)} to playlist`);
} catch (e) {
fail(`add: ${e.message}`);
}
},
async skip() {
try {
const data = await api.get('/playlist');
if (data.items.length === 0) { warn('Playlist is empty'); return; }
const idx = data.currentIndex;
if (idx < 0 || idx >= data.items.length - 1) {
warn('No next item in playlist');
return;
}
const next = data.items[idx + 1];
await api.put('/playlist/playing', { uid: next.uid });
info(`Skipped to: ${escBless(next.title)}`);
} catch (e) {
fail(`skip: ${e.message}`);
}
},
async clear() {
try {
await api.post('/playlist/clear');
info('Playlist cleared');
} catch (e) {
fail(`clear: ${e.message}`);
}
},
async kick(args) {
const parts = args.trim().split(/\s+/);
const name = parts[0];
const reason = parts.slice(1).join(' ') || 'Kicked by bot';
if (!name) { fail('Usage: /kick <name> [reason]'); return; }
try {
await api.post(`/users/${encodeURIComponent(name)}/kick`, { reason });
info(`Kicked ${escBless(name)}`);
} catch (e) {
fail(`kick: ${e.message}`);
}
},
me(args) {
const text = args.trim();
if (!text) { fail('Usage: /me <text>'); return; }
socket.emit('chatMsg', { msg: `/me ${text}` });
},
};
async function handleInput(line) {
line = line.trim();
if (!line) return;
if (line.startsWith('/')) {
const space = line.indexOf(' ');
const name = (space === -1 ? line.slice(1) : line.slice(1, space)).toLowerCase();
const args = space === -1 ? '' : line.slice(space + 1);
if (COMMANDS[name]) {
try {
await COMMANDS[name](args);
} catch (e) {
fail(`Command error: ${e.message}`);
}
} else {
fail(`Unknown command: /${name} (type /help for a list)`);
}
} else {
// Regular chat message — sent over the WebSocket, not REST.
// The server will echo it back as a chatMsg event.
socket.emit('chatMsg', { msg: line });
}
}
// ── Input box wiring ──────────────────────────────────────────────────────────
inputBox.on('submit', async (value) => {
inputBox.clearValue();
inputBox.focus();
screen.render();
if (value && value.trim()) {
await handleInput(value);
}
});
// Pressing Enter submits, but blessed's textbox needs this nudge
inputBox.key('enter', () => inputBox.submit());
// ── Boot ──────────────────────────────────────────────────────────────────────
info(`Connecting to ${SERVER_URL} as bot in #${CHANNEL}...`);
info('Type /help for available commands.');
inputBox.focus();
screen.render();

283
examples/demo-bot/package-lock.json generated Normal file
View file

@ -0,0 +1,283 @@
{
"name": "cytube-demo-bot",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cytube-demo-bot",
"version": "1.0.0",
"dependencies": {
"blessed": "^0.1.81",
"dotenv": "^16.0.0",
"node-fetch": "^2.7.0",
"socket.io-client": "^4.7.0"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/blessed": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz",
"integrity": "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==",
"bin": {
"blessed": "bin/tput.js"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/engine.io-client": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
}
},
"dependencies": {
"@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"blessed": {
"version": "0.1.81",
"resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz",
"integrity": "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ=="
},
"debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"requires": {
"ms": "^2.1.3"
}
},
"dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="
},
"engine.io-client": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
}
},
"socket.io-parser": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
}
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="
}
}
}

View file

@ -0,0 +1,15 @@
{
"name": "cytube-demo-bot",
"version": "1.0.0",
"description": "Demo bot for the CyTube Sync bot API — TUI chat client + REST queries",
"main": "bot.js",
"scripts": {
"start": "node bot.js"
},
"dependencies": {
"blessed": "^0.1.81",
"dotenv": "^16.0.0",
"node-fetch": "^2.7.0",
"socket.io-client": "^4.7.0"
}
}

View file

@ -0,0 +1,32 @@
# Python Show Bot Example
Uses `veretube-bot==0.1.4` with `AsyncBot` and the built-in Shows API helpers.
## Setup
```bash
cd examples/python-show-bot
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
Set environment variables:
- `BOT_TOKEN` (required)
- `CHANNEL` (required)
- `SOCKET_URL` (default: `http://localhost:1337`)
- `API_BASE` (default: `http://localhost:8080/api/v1`)
- `SHOW_TIMEZONE` (default: `UTC`, must be valid IANA timezone)
Run:
```bash
python bot.py
```
## Chat Commands
- `!shows` - list shows
- `!mkshow` - create a demo show
- `!runshow <id>` - run a show immediately

View file

@ -0,0 +1,116 @@
"""
Veretube Python Show Bot example (veretube-bot 0.1.4)
Commands in chat:
!shows -> list existing shows
!mkshow -> create an example show scheduled ~2 minutes from now
!show <id> -> inspect a show from API
!runshow <id> -> trigger immediate run of a show
"""
from datetime import datetime, timedelta, timezone
import asyncio
import os
from veretube_bot import AsyncBot, BotAPIError
BOT_TOKEN = os.getenv("BOT_TOKEN", "TOKEN_HERE")
CHANNEL = os.getenv("CHANNEL", "CHANNEL_NAME_HERE")
SOCKET_URL = os.getenv("SOCKET_URL", "http://localhost:1337")
API_BASE = os.getenv("API_BASE", "http://localhost:8080/api/v1")
SHOW_TIMEZONE = os.getenv("SHOW_TIMEZONE", "UTC")
bot = AsyncBot(
token=BOT_TOKEN,
channel=CHANNEL,
socket_url=SOCKET_URL,
api_url=API_BASE,
)
def create_example_show_payload() -> dict:
# Schedule a couple minutes in the future to make testing easy.
scheduled_for = datetime.now(timezone.utc) + timedelta(minutes=2)
return {
"name": f"Python Demo Show {scheduled_for.strftime('%H:%M:%S')}",
"scheduled_for": scheduled_for.isoformat(),
"timezone": SHOW_TIMEZONE,
"recurrence": "none",
"fill_mode": "append",
"conflict_mode": "force",
"start_playback": False,
"status": "scheduled",
"playlist": [
{"type": "yt", "id": "dQw4w9WgXcQ", "pos": "end"},
{"type": "yt", "id": "9bZkp7q19f0", "pos": "end"},
],
}
@bot.on("chatMsg")
async def on_chat(data):
msg = (data.get("msg") or "").strip()
if msg == "!shows":
try:
shows = await bot.list_shows()
except BotAPIError as err:
await bot.send_message(f"shows error: {err}")
return
if not shows:
await bot.send_message("No shows configured")
return
summary = ", ".join([f"#{s['id']} {s['name']} ({s['status']})" for s in shows[:4]])
await bot.send_message(f"Shows: {summary}")
elif msg == "!mkshow":
try:
show = await bot.create_show(create_example_show_payload())
persisted = await bot.get_show(show["id"])
await bot.send_message(
f"Created show #{persisted['id']} status={persisted.get('status')} "
f"scheduled_for={persisted.get('scheduled_for')} timezone={persisted.get('timezone')}"
)
except BotAPIError as err:
await bot.send_message(f"create show error: {err}")
elif msg.startswith("!runshow "):
parts = msg.split()
if len(parts) != 2 or not parts[1].isdigit():
await bot.send_message("Usage: !runshow <show_id>")
return
show_id = int(parts[1])
try:
result = await bot.show_action(show_id, "run")
await bot.send_message(f"Show #{result['id']} run action complete, status={result['status']}")
except BotAPIError as err:
await bot.send_message(f"run show error: {err}")
elif msg.startswith("!show "):
parts = msg.split()
if len(parts) != 2 or not parts[1].isdigit():
await bot.send_message("Usage: !show <show_id>")
return
show_id = int(parts[1])
try:
show = await bot.get_show(show_id)
await bot.send_message(
f"Show #{show['id']}: status={show.get('status')} "
f"scheduled_for={show.get('scheduled_for')} timezone={show.get('timezone')}"
)
except BotAPIError as err:
await bot.send_message(f"show lookup error: {err}")
@bot.on("changeMedia")
async def on_media(data):
title = data.get("title", "(unknown)")
await bot.send_message(f"Now playing: {title}")
if __name__ == "__main__":
asyncio.run(bot.run())

View file

@ -0,0 +1,23 @@
aiohappyeyeballs==2.6.2
aiohttp==3.13.5
aiosignal==1.4.0
async-timeout==5.0.1
attrs==26.1.0
bidict==0.23.1
certifi==2026.5.20
charset-normalizer==3.4.7
frozenlist==1.8.0
h11==0.16.0
idna==3.15
multidict==6.7.1
propcache==0.5.2
python-engineio==4.13.1
python-socketio==5.16.1
requests==2.33.1
simple-websocket==1.1.0
typing_extensions==4.15.0
urllib3==2.7.0
veretube-bot==0.1.4
websocket-client==1.9.0
wsproto==1.3.2
yarl==1.24.2

View file

@ -0,0 +1,26 @@
from veretube_bot import Bot, Rank
bot = Bot(
token="TOKEN_HERE",
channel="CHANNEL_NAME_HERE",
socket_url="http://localhost:1337",
api_url="http://localhost:8080/api/v1"
)
@bot.on("chatMsg")
def on_chat(data):
if data["msg"] == "!playlist":
playlist = bot.api.get_playlist()
bot.send_message(f"{len(playlist['items'])} items in queue")
elif data["msg"].startswith("!kick "):
name = data["msg"].split(" ", 1)[1]
bot.kick(name, "Kicked by command")
elif data["msg"] == "!emotes":
emotes = bot.get_emotes()
print(emotes)
@bot.on("changeMedia")
def on_media(data):
bot.send_message(f"Now playing: {data['title']}")
bot.run()

View file

@ -0,0 +1,13 @@
bidict==0.23.1
certifi==2026.4.22
charset-normalizer==3.4.7
h11==0.16.0
idna==3.13
python-engineio==4.13.1
python-socketio==5.16.1
requests==2.33.1
simple-websocket==1.1.0
urllib3==2.6.3
veretube-bot==0.1.0
websocket-client==1.9.0
wsproto==1.3.2

View file

@ -1,37 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var Filter = function(name, regex, flags, replace) {
this.name = name;
this.source = regex;
this.flags = flags;
this.regex = new RegExp(this.source, this.flags);
this.replace = replace;
this.active = true;
this.filterlinks = false;
}
Filter.prototype.pack = function() {
return {
name: this.name,
source: this.source,
flags: this.flags,
replace: this.replace,
active: this.active,
filterlinks: this.filterlinks
}
}
Filter.prototype.filter = function(text) {
return text.replace(this.regex, this.replace);
}
exports.Filter = Filter;

View file

@ -0,0 +1,243 @@
// ==UserScript==
// @name Google Drive Video Player for {SITENAME}
// @namespace gdcytube
// @description Play Google Drive videos on {SITENAME}
// {INCLUDE_BLOCK}
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @connect docs.google.com
// @run-at document-end
// @version 1.7.0
// ==/UserScript==
try {
function debug(message) {
try {
unsafeWindow.console.log('[Drive]', message);
} catch (error) {
unsafeWindow.console.error(error);
}
}
function httpRequest(opts) {
if (typeof GM_xmlhttpRequest === 'undefined') {
// Assume GM4.0
debug('Using GM4.0 GM.xmlHttpRequest');
GM.xmlHttpRequest(opts);
} else {
debug('Using old-style GM_xmlhttpRequest');
GM_xmlhttpRequest(opts);
}
}
var ITAG_QMAP = {
37: 1080,
46: 1080,
22: 720,
45: 720,
59: 480,
44: 480,
35: 480,
18: 360,
43: 360,
34: 360
};
var ITAG_CMAP = {
43: 'video/webm',
44: 'video/webm',
45: 'video/webm',
46: 'video/webm',
18: 'video/mp4',
22: 'video/mp4',
37: 'video/mp4',
59: 'video/mp4',
35: 'video/flv',
34: 'video/flv'
};
function getVideoInfo(id, cb) {
var url = 'https://docs.google.com/get_video_info?authuser='
+ '&docid=' + id
+ '&sle=true'
+ '&hl=en';
debug('Fetching ' + url);
httpRequest({
method: 'GET',
url: url,
onload: function (res) {
try {
debug('Got response ' + res.responseText);
if (res.status !== 200) {
debug('Response status not 200: ' + res.status);
return cb(
'Google Drive request failed: HTTP ' + res.status
);
}
var data = {};
var error;
// Google Santa sometimes eats login cookies and gets mad if there aren't any.
if(/accounts\.google\.com\/ServiceLogin/.test(res.responseText)){
error = 'Google Docs request failed: ' +
'This video requires you be logged into a Google account. ' +
'Open your Gmail in another tab and then refresh video.';
return cb(error);
}
res.responseText.split('&').forEach(function (kv) {
var pair = kv.split('=');
data[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
});
if (data.status === 'fail') {
error = 'Google Drive request failed: ' +
unescape(data.reason).replace(/\+/g, ' ');
return cb(error);
}
if (!data.fmt_stream_map) {
error = (
'Google has removed the video streams associated' +
' with this item. It can no longer be played.'
);
return cb(error);
}
data.links = {};
data.fmt_stream_map.split(',').forEach(function (item) {
var pair = item.split('|');
data.links[pair[0]] = pair[1];
});
data.videoMap = mapLinks(data.links);
cb(null, data);
} catch (error) {
unsafeWindow.console.error(error);
}
},
onerror: function () {
var error = 'Google Drive request failed: ' +
'metadata lookup HTTP request failed';
error.reason = 'HTTP_ONERROR';
return cb(error);
}
});
}
function mapLinks(links) {
var videos = {
1080: [],
720: [],
480: [],
360: []
};
Object.keys(links).forEach(function (itag) {
itag = parseInt(itag, 10);
if (!ITAG_QMAP.hasOwnProperty(itag)) {
return;
}
videos[ITAG_QMAP[itag]].push({
itag: itag,
contentType: ITAG_CMAP[itag],
link: links[itag]
});
});
return videos;
}
/*
* Greasemonkey 2.0 has this wonderful sandbox that attempts
* to prevent script developers from shooting themselves in
* the foot by removing the trigger from the gun, i.e. it's
* impossible to cross the boundary between the browser JS VM
* and the privileged sandbox that can run GM_xmlhttpRequest().
*
* So in this case, we have to resort to polling a special
* variable to see if getGoogleDriveMetadata needs to be called
* and deliver the result into another special variable that is
* being polled on the browser side.
*/
/*
* Browser side function -- sets gdUserscript.pollID to the
* ID of the Drive video to be queried and polls
* gdUserscript.pollResult for the result.
*/
function getGoogleDriveMetadata_GM(id, callback) {
debug('Setting GD poll ID to ' + id);
unsafeWindow.gdUserscript.pollID = id;
var tries = 0;
var i = setInterval(function () {
if (unsafeWindow.gdUserscript.pollResult) {
debug('Got result');
clearInterval(i);
var result = unsafeWindow.gdUserscript.pollResult;
unsafeWindow.gdUserscript.pollResult = null;
callback(result.error, result.result);
} else if (++tries > 100) {
// Took longer than 10 seconds, give up
clearInterval(i);
}
}, 100);
}
/*
* Sandbox side function -- polls gdUserscript.pollID for
* the ID of a Drive video to be queried, looks up the
* metadata, and stores it in gdUserscript.pollResult
*/
function setupGDPoll() {
unsafeWindow.gdUserscript = cloneInto({}, unsafeWindow);
var pollInterval = setInterval(function () {
if (unsafeWindow.gdUserscript.pollID) {
var id = unsafeWindow.gdUserscript.pollID;
unsafeWindow.gdUserscript.pollID = null;
debug('Polled and got ' + id);
getVideoInfo(id, function (error, data) {
unsafeWindow.gdUserscript.pollResult = cloneInto({
error: error,
result: data
}, unsafeWindow);
});
}
}, 1000);
}
var TM_COMPATIBLES = [
'Tampermonkey',
'Violentmonkey' // https://github.com/calzoneman/sync/issues/713
];
function isTampermonkeyCompatible() {
try {
return TM_COMPATIBLES.indexOf(GM_info.scriptHandler) >= 0;
} catch (error) {
return false;
}
}
if (isTampermonkeyCompatible()) {
unsafeWindow.getGoogleDriveMetadata = getVideoInfo;
} else {
debug('Using non-TM polling workaround');
unsafeWindow.getGoogleDriveMetadata = exportFunction(
getGoogleDriveMetadata_GM, unsafeWindow);
setupGDPoll();
}
unsafeWindow.console.log('Initialized userscript Google Drive player');
unsafeWindow.hasDriveUserscript = true;
// Checked against GS_VERSION from data.js
unsafeWindow.driveUserscriptVersion = '1.7';
} catch (error) {
unsafeWindow.console.error(error);
}

View file

@ -0,0 +1,37 @@
var fs = require('fs');
var path = require('path');
var sitename = process.argv[2];
var includes = process.argv.slice(3).map(function (include) {
return '// @include ' + include;
}).join('\n');
var lines = String(fs.readFileSync(
path.resolve(__dirname, 'cytube-google-drive.user.js'))).split('\n');
var userscriptOutput = '';
var metaOutput = '';
lines.forEach(function (line) {
if (line.match(/\{INCLUDE_BLOCK\}/)) {
userscriptOutput += includes + '\n';
} else if (line.match(/\{SITENAME\}/)) {
line = line.replace(/\{SITENAME\}/, sitename) + '\n';
userscriptOutput += line;
metaOutput += line;
} else {
if (line.match(/==\/?UserScript|@name|@version/)) {
metaOutput += line + '\n';
}
userscriptOutput += line + '\n';
}
});
fs.writeFileSync(
path.join(__dirname, '..', 'www', 'js', 'cytube-google-drive.user.js'),
userscriptOutput
);
fs.writeFileSync(
path.join(__dirname, '..', 'www', 'js', 'cytube-google-drive.meta.js'),
metaOutput
);

View file

@ -1,497 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var http = require("http");
var https = require("https");
var Logger = require("./logger.js");
var Media = require("./media.js").Media;
var CustomEmbedFilter = require("./customembed").filter;
module.exports = function (Server) {
function urlRetrieve(transport, options, callback) {
var req = transport.request(options, function (res) {
var buffer = "";
res.setEncoding("utf-8");
res.on("data", function (chunk) {
buffer += chunk;
});
res.on("end", function () {
callback(res.statusCode, buffer);
});
});
req.end();
}
var Getters = {
/* youtube.com */
yt: function (id, callback) {
if(Server.cfg["enable-ytv3"] && Server.cfg["ytv3apikey"]) {
Getters["ytv3"](id, callback);
return;
}
var options = {
host: "gdata.youtube.com",
port: 443,
path: "/feeds/api/videos/" + id + "?v=2&alt=json",
method: "GET",
dataType: "jsonp",
timeout: 1000
};
if(Server.cfg["ytv2devkey"]) {
options.headers = {
"X-Gdata-Key": "key=" + Server.cfg["ytv2devkey"]
};
}
urlRetrieve(https, options, function (status, data) {
if(status === 404) {
callback("Video not found", null);
return;
} else if(status === 403) {
callback("Private video", null);
return;
} else if(status !== 200) {
callback(true, null);
return;
}
try {
data = JSON.parse(data);
var seconds = data.entry.media$group.yt$duration.seconds;
var title = data.entry.title.$t;
var media = new Media(id, title, seconds, "yt");
callback(false, media);
} catch(e) {
// Gdata version 2 has the rather silly habit of
// returning error codes in XML when I explicitly asked
// for JSON
var m = buffer.match(/<internalReason>([^<]+)<\/internalReason>/);
if(m === null)
m = buffer.match(/<code>([^<]+)<\/code>/);
var err = true;
if(m) {
if(m[1] === "too_many_recent_calls") {
err = "YouTube is throttling the server right "+
"now for making too many requests. "+
"Please try again in a moment.";
} else {
err = m[1];
}
}
callback(err, null);
}
});
},
/* youtube.com API v3 (requires API key) */
ytv3: function (id, callback) {
var params = [
"part=" + encodeURIComponent("id,snippet,contentDetails"),
"id=" + id,
"key=" + Server.cfg["ytapikey"]
].join("&");
var options = {
host: "www.googleapis.com",
port: 443,
path: "/youtube/v3/videos?" + params,
method: "GET",
dataType: "jsonp",
timeout: 1000
};
urlRetrieve(https, options, function (status, data) {
if(status !== 200) {
callback(true, null);
return;
}
try {
data = JSON.parse(data);
// I am a bit disappointed that the API v3 just doesn't
// return anything in any error case
if(data.pageInfo.totalResults !== 1) {
callback(true, null);
return;
}
var vid = data.items[0];
var title = vid.snippet.title;
// No, it's not possible to get a number representing
// the video length. Instead, I get a time of the format
// PT#M#S which represents
// "Period of Time" # Minutes, # Seconds
var m = vid.contentDetails.duration.match(/PT(\d+)M(\d+)S/);
var seconds = parseInt(m[1]) * 60 + parseInt(m[2]);
var media = new Media(id, title, seconds, "yt");
callback(false, media);
} catch(e) {
callback(true, media);
}
});
},
/* youtube.com playlists */
yp: function (id, callback, url) {
var path = "/feeds/api/playlists/" + id + "?v=2&alt=json";
// YouTube only returns 25 at a time, so I have to keep asking
// for more with the URL they give me
if(url !== undefined) {
path = "/" + url.split("gdata.youtube.com")[1];
}
var options = {
host: "gdata.youtube.com",
port: 443,
path: path,
method: "GET",
dataType: "jsonp",
timeout: 1000
};
if(Server.cfg["ytv2devkey"]) {
options.headers = {
"X-Gdata-Key": "key=" + Server.cfg["ytv2devkey"]
};
}
urlRetrieve(https, options, function (status, data) {
if(status === 404) {
callback("Playlist not found", null);
return;
} else if(status === 403) {
callback("Playlist is private", null);
return;
} else if(status !== 200) {
callback(true, null);
}
try {
data = JSON.parse(data);
var vids = [];
for(var i in data.feed.entry) {
try {
var item = data.feed.entry[i];
var id = item.media$group.yt$videoid.$t;
var title = item.title.$t;
var seconds = item.media$group.yt$duration.seconds;
var media = new Media(id, title, seconds, "yt");
vids.push(media);
} catch(e) {
}
}
callback(false, vids);
var links = data.feed.link;
for(var i in links) {
if(links[i].rel === "next")
Getters["yp"](id, callback, links[i].href);
}
} catch(e) {
callback(true, null);
}
});
},
/* youtube.com search */
ytSearch: function (terms, callback) {
for(var i in terms)
terms[i] = encodeURIComponent(terms[i]);
var query = terms.join("+");
var options = {
host: "gdata.youtube.com",
port: 443,
path: "/feeds/api/videos/?q=" + query + "&v=2&alt=json",
method: "GET",
dataType: "jsonp",
timeout: 1000
};
if(Server.cfg["ytv2devkey"]) {
options.headers = {
"X-Gdata-Key": "key=" + Server.cfg["ytv2devkey"]
};
}
urlRetrieve(https, options, function (status, data) {
if(status !== 200) {
callback(true, null);
return;
}
try {
data = JSON.parse(data);
var vids = [];
for(var i in data.feed.entry) {
try {
var item = data.feed.entry[i];
var id = item.media$group.yt$videoid.$t;
var title = item.title.$t;
var seconds = item.media$group.yt$duration.seconds;
var media = new Media(id, title, seconds, "yt");
media.thumb = item.media$group.media$thumbnail[0];
vids.push(media);
} catch(e) {
}
}
callback(false, vids);
} catch(e) {
callback(true, null);
}
});
},
/* vimeo.com */
vi: function (id, callback) {
var options = {
host: "vimeo.com",
port: 443,
path: "/api/v2/video/" + id + ".json",
method: "GET",
dataType: "jsonp",
timeout: 1000
};
urlRetrieve(https, options, function (status, data) {
if(status === 404) {
callback("Video not found", null);
return;
} else if(status === 403) {
callback("Private video", null);
return;
} else if(status !== 200) {
callback(true, null);
return;
}
try {
data = JSON.parse(data);
data = data[0];
var seconds = data.duration;
var title = data.title;
var media = new Media(id, title, seconds, "vi");
callback(false, media);
} catch(e) {
var err = true;
if(buffer.match(/not found/))
err = "Video not found";
callback(err, null);
}
});
},
/* dailymotion.com */
dm: function (id, callback) {
// Dailymotion's API is an example of an API done right
// - Supports SSL
// - I can ask for exactly which fields I want
// - URL is simple
// - Field names are sensible
// Other media providers take notes, please
var options = {
host: "api.dailymotion.com",
port: 443,
path: "/video/" + id + "?fields=duration,title",
method: "GET",
dataType: "jsonp",
timeout: 1000
};
urlRetrieve(https, options, function (status, data) {
if(status !== 200) {
callback(true, null);
return;
}
try {
data = JSON.parse(data);
var title = data.title;
var seconds = data.duration;
if(title === "Deleted video" && seconds === 10) {
callback("Video not found", null);
return;
}
var media = new Media(id, title, seconds, "dm");
callback(false, media);
} catch(e) {
callback(err, null);
}
});
},
/* soundcloud.com */
sc: function (id, callback) {
// Soundcloud's API is badly designed and badly documented
// In order to lookup track data from a URL, I have to first
// make a call to /resolve to get the track id, then make a second
// call to /tracks/{track.id} to actally get useful data
// This is a waste of bandwidth and a pain in the ass
const SC_CLIENT = "2e0c82ab5a020f3a7509318146128abd";
var options = {
host: "api.soundcloud.com",
port: 443,
path: "/resolve.json?url=" + id + "&client_id=" + SC_CLIENT,
method: "GET",
dataType: "jsonp",
timeout: 1000
};
urlRetrieve(https, options, function (status, data) {
if(status === 404) {
callback("Sound not found", null);
return;
} else if(status !== 302) {
callback(true, null);
return;
}
var track = null;
try {
data = JSON.parse(data);
track = data.location;
} catch(e) {
callback(true, null);
return;
}
var options2 = {
host: "api.soundcloud.com",
port: 443,
path: track,
method: "GET",
dataType: "jsonp",
timeout: 1000
};
// I want to get off async's wild ride
urlRetrieve(https, options2, function (status, data) {
if(status !== 200) {
callback(true, null);
return;
}
try {
data = JSON.parse(data);
// Duration is in ms, but I want s
var seconds = data.duration / 1000;
var title = data.title;
var media = new Media(id, title, seconds, "sc");
callback(false, media);
} catch(e) {
callback(true, null);
}
});
});
},
/* livestream.com */
li: function (id, callback) {
var title = "Livestream.com - " + id;
var media = new Media(id, title, "--:--", "li");
callback(false, media);
},
/* twitch.tv */
tw: function (id, callback) {
var title = "Twitch.tv - " + id;
var media = new Media(id, title, "--:--", "tw");
callback(false, media);
},
/* justin.tv */
jt: function (id, callback) {
var title = "Justin.tv - " + id;
var media = new Media(id, title, "--:--", "jt");
callback(false, media);
},
/* ustream.tv */
us: function (id, callback) {
var options = {
host: "www.ustream.tv",
port: 80,
path: "/" + id,
method: "GET",
timeout: 1000
};
urlRetrieve(http, options, function (status, data) {
if(status !== 200) {
callback(true, null);
return;
}
// Regexing the ID out of the HTML because
// Ustream's API is so horribly documented
// I literally could not figure out how to retrieve
// this information.
//
// [](/eatadick)
var m = data.match(/cid":([0-9]+)/);
if(m) {
var title = "Ustream.tv - " + id;
var media = new Media(m[1], title, "--:--", "us");
callback(false, media);
} else {
callback(true, null);
}
});
},
/* JWPlayer */
jw: function (id, callback) {
var title = "JWPlayer - " + id;
var media = new Media(id, title, "--:--", "jw");
callback(false, media);
},
/* rtmp stream */
rt: function (id, callback) {
var title = "Livestream";
var media = new Media(id, title, "--:--", "rt");
callback(false, media);
},
/* imgur.com albums */
im: function (id, callback) {
var title = "Imgur Album - " + id;
var media = new Media(id, title, "--:--", "im");
callback(false, media);
},
/* custom embed */
cu: function (id, callback) {
id = CustomEmbedFilter(id);
var media = new Media(id, "Custom Media", "--:--", "cu");
callback(false, media);
}
};
return {
Getters: Getters,
getMedia: function (id, type, callback) {
if(type in this.Getters) {
this.Getters[type](id, callback);
}
}
};
}

98
index.js Executable file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env node
const ver = process.version.match(/v(\d+)\.\d+\.\d+/);
if (parseInt(ver[1], 10) < 12) {
console.error(
`node.js ${process.version} is not supported. ` +
'CyTube requires node v12 or later.'
)
process.exit(1);
}
checkPlayerExists();
const args = parseArgs();
if (args.has('--daemonize')) {
fork();
} else {
try {
require('./lib/main');
} catch (err) {
console.error('FATAL: Failed to require() lib/main.js');
handleStartupError(err);
}
}
function fork() {
try {
console.log('Warning: --daemonize support is experimental. Use with caution.');
const spawn = require('child_process').spawn;
const path = require('path');
const main = path.resolve(__dirname, 'lib', 'main.js');
const child = spawn(process.argv[0], [main], {
detached: true,
stdio: 'ignore' // TODO: support setting stdout/stderr logfile
});
child.unref();
console.log('Forked with PID ' + child.pid);
} catch (error) {
console.error('FATAL: Failed to fork lib/main.js');
handleStartupError(error);
}
}
function handleStartupError(err) {
if (/module version mismatch/i.test(err.message)) {
console.error('Module version mismatch, try running `npm rebuild` or ' +
'removing the node_modules folder and re-running ' +
'`npm install`');
} else {
console.error('Possible causes:\n' +
' * You haven\'t run `npm run build-server` to regenerate ' +
'the runtime\n' +
' * You\'ve upgraded node/npm and haven\'t rebuilt dependencies ' +
'(try `npm rebuild` or `rm -rf node_modules && npm install`)\n' +
' * A dependency failed to install correctly (check the output ' +
'of `npm install` next time)');
}
console.error(err.stack);
process.exit(1);
}
function parseArgs() {
const args = new Map();
for (var i = 2; i < process.argv.length; i++) {
if (/^--/.test(process.argv[i])) {
var val;
if (i+1 < process.argv.length) val = process.argv[i+1];
else val = null;
args.set(process.argv[i], val);
}
}
return args;
}
function checkPlayerExists() {
const fs = require('fs');
const path = require('path');
const playerDotJs = path.join(__dirname, 'www', 'js', 'player.js');
if (!fs.existsSync(playerDotJs)) {
console.error(
'Missing video player: www/js/player.js. This should have been ' +
'automatically generated by the postinstall step of ' +
'`npm install`, but you can manually regenerate it by running ' +
'`npm run build-player`'
);
process.exit(1);
}
}

View file

@ -0,0 +1,603 @@
const assert = require('assert');
const KickbanModule = require('../../lib/channel/kickban');
const database = require('../../lib/database');
const Promise = require('bluebird');
const testDB = require('../testutil/db').testDB;
database.init(testDB);
describe('KickbanModule', () => {
const channelName = `test_${Math.random().toString(31).substring(2)}`;
let mockChannel;
let mockUser;
let kickban;
beforeEach(() => {
mockChannel = {
name: channelName,
refCounter: {
ref() { },
unref() { }
},
logger: {
log() { }
},
modules: {
permissions: {
canBan() {
return true;
}
}
},
users: []
};
mockUser = {
getName() {
return 'The_Admin';
},
getLowerName() {
return 'the_admin';
},
socket: {
emit(frame) {
if (frame === 'errorMsg') {
throw new Error(arguments[1].msg);
}
}
},
account: {
effectiveRank: 3
}
};
kickban = new KickbanModule(mockChannel);
});
afterEach(async () => {
await database.getDB().runTransaction(async tx => {
await tx.table('channel_bans')
.where({ channel: channelName })
.del();
await tx.table('channel_ranks')
.where({ channel: channelName })
.del();
});
});
describe('#handleCmdBan', () => {
it('inserts a valid ban', done => {
let kicked = false;
mockChannel.refCounter.unref = () => {
assert(kicked, 'Expected user to be kicked');
database.getDB().runTransaction(async tx => {
const ban = await tx.table('channel_bans')
.where({
channel: channelName,
name: 'test_user'
})
.first();
assert.strictEqual(ban.ip, '*');
assert.strictEqual(ban.reason, 'because reasons');
assert.strictEqual(ban.bannedby, mockUser.getName());
done();
});
};
mockChannel.users = [{
getLowerName() {
return 'test_user';
},
kick(reason) {
assert.strictEqual(reason, "You're banned!");
kicked = true;
}
}];
kickban.handleCmdBan(
mockUser,
'/ban test_user because reasons',
{}
);
});
it('rejects if the username is invalid', done => {
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
'Invalid username'
);
done();
}
};
kickban.handleCmdBan(
mockUser,
'/ban test_user<>%$# because reasons',
{}
);
});
it('rejects if the user does not have ban permission', done => {
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
'You do not have ban permissions on this channel'
);
done();
}
};
mockChannel.modules.permissions.canBan = () => false;
kickban.handleCmdBan(
mockUser,
'/ban test_user because reasons',
{}
);
});
it('rejects if the user tries to ban themselves', done => {
let costanza = false;
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
'You cannot ban yourself'
);
if (!costanza) {
throw new Error('Expected costanza for banning self');
}
done();
} else if (frame === 'costanza') {
assert.strictEqual(
obj.msg,
"You can't ban yourself"
);
costanza = true;
}
};
kickban.handleCmdBan(
mockUser,
'/ban the_Admin because reasons',
{}
);
});
it('rejects if the user is ranked below the ban recipient', done => {
database.getDB().runTransaction(tx => {
return tx.table('channel_ranks')
.insert({
channel: channelName,
name: 'test_user',
rank: 5
});
}).then(() => {
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
"You don't have permission to ban test_user"
);
done();
}
};
kickban.handleCmdBan(
mockUser,
'/ban test_user because reasons',
{}
);
});
});
it('rejects if the the ban recipient is already banned', done => {
database.getDB().runTransaction(tx => {
return tx.table('channel_bans')
.insert({
channel: channelName,
name: 'test_user',
ip: '*',
bannedby: 'somebody',
reason: 'I dunno'
});
}).then(() => {
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
'test_user is already banned'
);
done();
}
};
kickban.handleCmdBan(
mockUser,
'/ban test_user because reasons',
{}
);
});
});
});
describe('#handleCmdIPBan', () => {
beforeEach(async () => {
await database.getDB().runTransaction(async tx => {
await tx.table('aliases')
.insert([{
name: 'test_user',
ip: '1.2.3.4',
time: Date.now()
}]);
});
});
afterEach(async () => {
await database.getDB().runTransaction(async tx => {
await tx.table('aliases')
.where({ name: 'test_user' })
.orWhere({ ip: '1.2.3.4' })
.del();
});
});
it('inserts a valid ban', done => {
let firstUserKicked = false;
let secondUserKicked = false;
mockChannel.refCounter.unref = () => {
assert(firstUserKicked, 'Expected banned user to be kicked');
assert(
secondUserKicked,
'Expected user with banned IP to be kicked'
);
database.getDB().runTransaction(async tx => {
const nameBan = await tx.table('channel_bans')
.where({
channel: channelName,
name: 'test_user',
ip: '*'
})
.first();
assert.strictEqual(nameBan.reason, 'because reasons');
assert.strictEqual(nameBan.bannedby, mockUser.getName());
const ipBan = await tx.table('channel_bans')
.where({
channel: channelName,
ip: '1.2.3.4'
})
.first();
assert.strictEqual(ipBan.name, 'test_user');
assert.strictEqual(ipBan.reason, 'because reasons');
assert.strictEqual(ipBan.bannedby, mockUser.getName());
done();
});
};
mockChannel.users = [{
getLowerName() {
return 'test_user';
},
realip: '1.2.3.4',
kick(reason) {
assert.strictEqual(reason, "You're banned!");
firstUserKicked = true;
}
}, {
getLowerName() {
return 'second_user_same_ip';
},
realip: '1.2.3.4',
kick(reason) {
assert.strictEqual(reason, "You're banned!");
secondUserKicked = true;
}
}];
kickban.handleCmdIPBan(
mockUser,
'/ipban test_user because reasons',
{}
);
});
it('inserts a valid range ban', done => {
mockChannel.refCounter.unref = () => {
database.getDB().runTransaction(async tx => {
const ipBan = await tx.table('channel_bans')
.where({
channel: channelName,
ip: '1.2.3'
})
.first();
assert.strictEqual(ipBan.name, 'test_user');
assert.strictEqual(ipBan.reason, 'because reasons');
assert.strictEqual(ipBan.bannedby, mockUser.getName());
done();
});
};
kickban.handleCmdIPBan(
mockUser,
'/ipban test_user range because reasons',
{}
);
});
it('inserts a valid wide-range ban', done => {
mockChannel.refCounter.unref = () => {
database.getDB().runTransaction(async tx => {
const ipBan = await tx.table('channel_bans')
.where({
channel: channelName,
ip: '1.2'
})
.first();
assert.strictEqual(ipBan.name, 'test_user');
assert.strictEqual(ipBan.reason, 'because reasons');
assert.strictEqual(ipBan.bannedby, mockUser.getName());
done();
});
};
kickban.handleCmdIPBan(
mockUser,
'/ipban test_user wrange because reasons',
{}
);
});
it('inserts a valid IPv6 ban', done => {
const longIP = require('../../lib/utilities').expandIPv6('::abcd');
mockChannel.refCounter.unref = () => {
database.getDB().runTransaction(async tx => {
const ipBan = await tx.table('channel_bans')
.where({
channel: channelName,
ip: longIP
})
.first();
assert.strictEqual(ipBan.name, 'test_user');
assert.strictEqual(ipBan.reason, 'because reasons');
assert.strictEqual(ipBan.bannedby, mockUser.getName());
done();
});
};
database.getDB().runTransaction(async tx => {
await tx.table('aliases')
.insert({
name: 'test_user',
ip: longIP,
time: Date.now()
});
}).then(() => {
kickban.handleCmdIPBan(
mockUser,
'/ipban test_user because reasons',
{}
);
});
});
it('rejects if the user does not have ban permission', done => {
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
'You do not have ban permissions on this channel'
);
done();
}
};
mockChannel.modules.permissions.canBan = () => false;
kickban.handleCmdIPBan(
mockUser,
'/ipban test_user because reasons',
{}
);
});
it('rejects if the user tries to ban themselves', done => {
let costanza = false;
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
'You cannot ban yourself'
);
if (!costanza) {
throw new Error('Expected costanza for banning self');
}
done();
} else if (frame === 'costanza') {
assert.strictEqual(
obj.msg,
"You can't ban yourself"
);
costanza = true;
}
};
kickban.handleCmdIPBan(
mockUser,
'/ipban the_Admin because reasons',
{}
);
});
it('rejects if the user is ranked below the ban recipient', done => {
database.getDB().runTransaction(tx => {
return tx.table('channel_ranks')
.insert({
channel: channelName,
name: 'test_user',
rank: 5
});
}).then(() => {
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
"You don't have permission to ban IP " +
"09l.TFb.5To.HBB"
);
done();
}
};
kickban.handleCmdIPBan(
mockUser,
'/ipban test_user because reasons',
{}
);
});
});
it('rejects if the user is ranked below an alias of the ban recipient', done => {
database.getDB().runTransaction(async tx => {
await tx.table('channel_ranks')
.insert({
channel: channelName,
name: 'another_user',
rank: 5
});
await tx.table('aliases')
.insert({
name: 'another_user',
ip: '1.2.3.3', // different IP, same /24 range
time: Date.now()
});
}).then(() => {
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
"You don't have permission to ban IP " +
"09l.TFb.5To.*"
);
done();
}
};
kickban.handleCmdIPBan(
mockUser,
'/ipban test_user range because reasons',
{}
);
});
});
it('rejects if the the ban recipient IP is already banned', done => {
database.getDB().runTransaction(tx => {
return tx.table('channel_bans')
.insert({
channel: channelName,
name: 'another_user',
ip: '1.2.3.4',
bannedby: 'somebody',
reason: 'I dunno'
});
}).then(() => {
mockUser.socket.emit = (frame, obj) => {
if (frame === 'errorMsg') {
assert.strictEqual(
obj.msg,
'09l.TFb.5To.HBB is already banned'
);
done();
}
};
kickban.handleCmdIPBan(
mockUser,
'/ipban test_user because reasons',
{}
);
});
});
it('still adds the IP ban even if the name is already banned', done => {
mockChannel.refCounter.unref = () => {
database.getDB().runTransaction(async tx => {
const ipBan = await tx.table('channel_bans')
.where({
channel: channelName,
ip: '1.2.3.4'
})
.first();
assert.strictEqual(ipBan.name, 'test_user');
assert.strictEqual(ipBan.reason, 'because reasons');
assert.strictEqual(ipBan.bannedby, mockUser.getName());
done();
});
};
database.getDB().runTransaction(tx => {
return tx.table('channel_bans')
.insert({
channel: channelName,
name: 'test_user',
ip: '*',
bannedby: 'somebody',
reason: 'I dunno'
});
}).then(() => {
kickban.handleCmdIPBan(
mockUser,
'/ipban test_user because reasons',
{}
);
});
});
});
});

View file

@ -0,0 +1,109 @@
const assert = require('assert');
const { BannedChannelsController } = require('../../lib/controller/banned-channels');
const dbChannels = require('../../lib/database/channels');
const testDB = require('../testutil/db').testDB;
const { EventEmitter } = require('events');
require('../../lib/database').init(testDB);
const testBan = {
name: 'ban_test_1',
externalReason: 'because I said so',
internalReason: 'illegal content',
bannedBy: 'admin'
};
async function cleanupTestBan() {
return dbChannels.removeBannedChannel(testBan.name);
}
describe('BannedChannelsController', () => {
let controller;
let messages;
beforeEach(async () => {
await cleanupTestBan();
messages = new EventEmitter();
controller = new BannedChannelsController(
dbChannels,
messages
);
});
afterEach(async () => {
await cleanupTestBan();
});
it('bans a channel', async () => {
assert.strictEqual(await controller.getBannedChannel(testBan.name), null);
let received = null;
messages.once('ChannelBanned', cb => {
received = cb;
});
await controller.banChannel(testBan);
let info = await controller.getBannedChannel(testBan.name);
for (let field of Object.keys(testBan)) {
// Consider renaming parameter to avoid this branch
if (field === 'name') {
assert.strictEqual(info.channelName, testBan.name);
} else {
assert.strictEqual(info[field], testBan[field]);
}
}
assert.notEqual(received, null);
assert.strictEqual(received.channel, testBan.name);
assert.strictEqual(received.externalReason, testBan.externalReason);
});
it('updates an existing ban', async () => {
let received = [];
messages.on('ChannelBanned', cb => {
received.push(cb);
});
await controller.banChannel(testBan);
let testBan2 = { ...testBan, externalReason: 'because of reasons' };
await controller.banChannel(testBan2);
let info = await controller.getBannedChannel(testBan2.name);
for (let field of Object.keys(testBan2)) {
// Consider renaming parameter to avoid this branch
if (field === 'name') {
assert.strictEqual(info.channelName, testBan2.name);
} else {
assert.strictEqual(info[field], testBan2[field]);
}
}
assert.deepStrictEqual(received, [
{
channel: testBan.name,
externalReason: testBan.externalReason
},
{
channel: testBan2.name,
externalReason: testBan2.externalReason
},
]);
});
it('unbans a channel', async () => {
let received = null;
messages.once('ChannelUnbanned', cb => {
received = cb;
});
await controller.banChannel(testBan);
await controller.unbanChannel(testBan.name, testBan.bannedBy);
let info = await controller.getBannedChannel(testBan.name);
assert.strictEqual(info, null);
assert.notEqual(received, null);
assert.strictEqual(received.channel, testBan.name);
});
});

View file

@ -0,0 +1,88 @@
const assert = require('assert');
const { testDB } = require('../testutil/db');
const accounts = require('../../lib/database/accounts');
require('../../lib/database').init(testDB);
describe('AccountsDatabase', () => {
describe('#verifyLogin', () => {
let ip = '169.254.111.111';
let user;
let password;
beforeEach(async () => {
return testDB.knex.table('users')
.where({ ip })
.delete();
});
beforeEach(done => {
user = `u${Math.random().toString(31).substring(2)}`;
password = 'int!gration_Test';
accounts.register(
user,
password,
'',
ip,
(error, res) => {
if (error) {
throw error;
}
console.log(`Created test user ${user}`);
done();
}
)
});
it('verifies a correct login', done => {
accounts.verifyLogin(
user,
password,
(error, res) => {
if (error) {
throw error;
}
assert.strictEqual(res.name, user);
done();
}
);
});
it('verifies a correct login with an older hash', done => {
testDB.knex.table('users')
.where({ name: user })
.update({
// 'test' hashed with old version of bcrypt module
password: '$2b$10$2oCG7O9FFqie7T8O33yQDugFPS0NqkgbQjtThTs7Jr8E1QOzdRruK'
})
.then(() => {
accounts.verifyLogin(
user,
'test',
(error, res) => {
if (error) {
throw error;
}
assert.strictEqual(res.name, user);
done();
}
);
});
});
it('rejects an incorrect login', done => {
accounts.verifyLogin(
user,
'not the right password',
(error, res) => {
assert.strictEqual(error, 'Invalid username/password combination');
done();
}
);
});
});
});

View file

@ -0,0 +1,76 @@
const assert = require('assert');
const AliasesDB = require('../../lib/db/aliases').AliasesDB;
const testDB = require('../testutil/db').testDB;
const aliasesDB = new AliasesDB(testDB);
const testIPs = ['111.111.111.111', '111.111.111.222'];
const testNames = ['itest1', 'itest2'];
function cleanup() {
return testDB.knex.table('aliases')
.where('ip', 'in', testIPs)
.del()
.then(() => {
return testDB.knex.table('aliases')
.where('name', 'in', testNames)
.del();
});
}
function addSomeAliases() {
return cleanup().then(() => {
return testDB.knex.table('aliases')
.insert([
{ ip: testIPs[0], name: testNames[0], time: Date.now() },
{ ip: testIPs[0], name: testNames[1], time: Date.now() },
{ ip: testIPs[1], name: testNames[1], time: Date.now() }
]);
});
}
describe('AliasesDB', () => {
describe('#addAlias', () => {
beforeEach(cleanup);
afterEach(cleanup);
it('adds a new alias', () => {
return aliasesDB.addAlias(testIPs[0], testNames[0])
.then(() => {
return testDB.knex.table('aliases')
.where({ ip: testIPs[0], name: testNames[0] })
.select()
.then(rows => {
assert.strictEqual(rows.length, 1, 'expected 1 row');
});
});
});
});
describe('#getAliasesByIP', () => {
beforeEach(addSomeAliases);
afterEach(cleanup);
it('retrieves aliases by IP', () => {
return aliasesDB.getAliasesByIP(testIPs[0])
.then(names => assert.deepStrictEqual(
names.sort(), testNames.sort()));
});
it('retrieves aliases by partial IP', () => {
return aliasesDB.getAliasesByIP(testIPs[0].substring(4))
.then(names => assert.deepStrictEqual(
names.sort(), testNames.sort()));
});
});
describe('#getIPsByName', () => {
beforeEach(addSomeAliases);
afterEach(cleanup);
it('retrieves IPs by name', () => {
return aliasesDB.getIPsByName(testNames[1])
.then(ips => assert.deepStrictEqual(
ips.sort(), testIPs.sort()));
});
});
});

View file

@ -0,0 +1,92 @@
const assert = require('assert');
const GlobalBanDB = require('../../lib/db/globalban').GlobalBanDB;
const testDB = require('../testutil/db').testDB;
const { o } = require('../testutil/o');
const globalBanDB = new GlobalBanDB(testDB);
const testBan = { ip: '8.8.8.8', reason: 'test' };
function cleanupTestBan() {
return testDB.knex.table('global_bans')
.where({ ip: testBan.ip })
.del();
}
function setupTestBan() {
return testDB.knex.table('global_bans')
.insert(testBan)
.catch(error => {
if (error.code === 'ER_DUP_ENTRY') {
return testDB.knex.table('global_bans')
.where({ ip: testBan.ip })
.update({ reason: testBan.reason });
}
throw error;
});
}
describe('GlobalBanDB', () => {
describe('#listGlobalBans', () => {
beforeEach(setupTestBan);
afterEach(cleanupTestBan);
it('lists existing IP bans', () => {
return globalBanDB.listGlobalBans().then(bans => {
assert.deepStrictEqual([{
ip: '8.8.8.8',
reason: 'test'
}], bans.map(o));
});
});
});
describe('#addGlobalIPBan', () => {
beforeEach(cleanupTestBan);
afterEach(cleanupTestBan);
it('adds a new ban', () => {
return globalBanDB.addGlobalIPBan('8.8.8.8', 'test').then(() => {
return testDB.knex.table('global_bans')
.where({ ip: '8.8.8.8' })
.select()
.then(rows => {
assert.strictEqual(rows.length, 1, 'Expected 1 row');
assert.strictEqual(rows[0].ip, '8.8.8.8');
assert.strictEqual(rows[0].reason, 'test');
});
});
});
it('updates the reason on an existing ban', () => {
return globalBanDB.addGlobalIPBan('8.8.8.8', 'test').then(() => {
return globalBanDB.addGlobalIPBan('8.8.8.8', 'different').then(() => {
return testDB.knex.table('global_bans')
.where({ ip: '8.8.8.8' })
.select()
.then(rows => {
assert.strictEqual(rows.length, 1, 'Expected 1 row');
assert.strictEqual(rows[0].ip, '8.8.8.8');
assert.strictEqual(rows[0].reason, 'different');
});
});
});
});
});
describe('#removeGlobalIPBan', () => {
beforeEach(setupTestBan);
afterEach(cleanupTestBan);
it('removes a ban', () => {
return globalBanDB.removeGlobalIPBan('8.8.8.8').then(() => {
return testDB.knex.table('global_bans')
.where({ ip: '8.8.8.8' })
.select()
.then(rows => {
assert.strictEqual(rows.length, 0, 'Expected 0 rows');
});
});
});
});
});

View file

@ -0,0 +1,144 @@
const assert = require('assert');
const PasswordResetDB = require('../../lib/db/password-reset').PasswordResetDB;
const testDB = require('../testutil/db').testDB;
const { o } = require('../testutil/o');
const passwordResetDB = new PasswordResetDB(testDB);
function cleanup() {
return testDB.knex.table('password_reset').del();
}
describe('PasswordResetDB', () => {
describe('#insert', () => {
beforeEach(cleanup);
const params = {
ip: '1.2.3.4',
name: 'testing',
email: 'test@example.com',
hash: 'abcdef',
expire: 5678
};
it('adds a new password reset', () => {
return passwordResetDB.insert(params).then(() => {
return testDB.knex.table('password_reset')
.where({ name: 'testing' })
.select();
}).then(rows => {
assert.strictEqual(rows.length, 1);
assert.deepStrictEqual(o(rows[0]), params);
});
});
it('overwrites an existing reset for the same name', () => {
return passwordResetDB.insert(params).then(() => {
params.ip = '5.6.7.8';
params.email = 'somethingelse@example.com';
params.hash = 'qwertyuiop';
params.expire = 9999;
return passwordResetDB.insert(params);
}).then(() => {
return testDB.knex.table('password_reset')
.where({ name: 'testing' })
.select();
}).then(rows => {
assert.strictEqual(rows.length, 1);
assert.deepStrictEqual(o(rows[0]), params);
});
});
});
describe('#get', () => {
const reset = {
ip: '1.2.3.4',
name: 'testing',
email: 'test@example.com',
hash: 'abcdef',
expire: 5678
};
beforeEach(() => cleanup().then(() => {
return testDB.knex.table('password_reset').insert(reset);
}));
it('gets a password reset by hash', () => {
return passwordResetDB.get(reset.hash).then(result => {
assert.deepStrictEqual(o(result), reset);
});
});
it('throws when no reset exists for the input', () => {
return passwordResetDB.get('lalala').then(() => {
assert.fail('Expected not found error');
}).catch(error => {
assert.strictEqual(
error.message,
'No password reset found for hash lalala'
);
});
});
});
describe('#delete', () => {
const reset = {
ip: '1.2.3.4',
name: 'testing',
email: 'test@example.com',
hash: 'abcdef',
expire: 5678
};
beforeEach(() => cleanup().then(() => {
return testDB.knex.table('password_reset').insert(reset);
}));
it('deletes a password reset by hash', () => {
return passwordResetDB.delete(reset.hash).then(() => {
return testDB.knex.table('password_reset')
.where({ name: 'testing' })
.select();
}).then(rows => {
assert.strictEqual(rows.length, 0);
});
});
});
describe('#cleanup', () => {
const now = Date.now();
const reset1 = {
ip: '1.2.3.4',
name: 'testing',
email: 'test@example.com',
hash: 'abcdef',
expire: now - 25 * 60 * 60 * 1000
};
const reset2 = {
ip: '5.6.7.8',
name: 'testing2',
email: 'test@example.com',
hash: 'abcdef',
expire: now
};
beforeEach(() => cleanup().then(() => {
return testDB.knex.table('password_reset')
.insert([reset1, reset2]);
}));
it('cleans up old password resets', () => {
return passwordResetDB.cleanup().then(() => {
return testDB.knex.table('password_reset')
.whereIn('name', ['testing1', 'testing2'])
.select();
}).then(rows => {
assert.strictEqual(rows.length, 1);
assert.deepStrictEqual(o(rows[0]), reset2);
});
});
});
});

View file

@ -0,0 +1,136 @@
const assert = require('assert');
const KickbanModule = require('../../lib/channel/kickban');
const database = require('../../lib/database');
const dbChannels = require('../../lib/database/channels');
const Promise = require('bluebird');
const ChannelModule = require('../../lib/channel/module');
const Flags = require('../../lib/flags');
const testDB = require('../testutil/db').testDB;
function randomString(length) {
const chars = 'abcdefgihkmnpqrstuvwxyz0123456789';
let str = '';
for (let i = 0; i < length; i++) {
str += chars[Math.floor(Math.random() * chars.length)];
}
return str;
}
database.init(testDB);
describe('onPreUserJoin Ban Check', () => {
const channelName = `test_${randomString(20)}`;
const bannedIP = '1.1.1.1';
const bannedName = 'troll';
const mockChannel = {
name: channelName,
modules: {},
is(flag) {
return flag === Flags.C_REGISTERED;
}
};
const module = new KickbanModule(mockChannel);
before(done => {
dbChannels.ban(channelName, bannedIP, bannedName, '', '', () => {
dbChannels.ban(channelName, bannedIP, '', '', '', () => {
dbChannels.ban(channelName, '*', bannedName, '', '', () => {
done();
});
});
});
});
after(done => {
dbChannels.deleteBans(channelName, null, () => {
done();
});
});
it('handles a banned IP with a different name', done => {
const user = {
getName() {
return 'anotherTroll';
},
realip: bannedIP,
kick() {
}
};
module.onUserPreJoin(user, null, (error, res) => {
assert.equal(error, null, `Unexpected error: ${error}`);
assert.equal(res, ChannelModule.DENY, 'Expected user to be banned');
done();
});
});
it('handles a banned name with a different IP', done => {
const user = {
getName() {
return 'troll';
},
realip: '5.5.5.5',
kick() {
}
};
module.onUserPreJoin(user, null, (error, res) => {
assert.equal(error, null, `Unexpected error: ${error}`);
assert.equal(res, ChannelModule.DENY, 'Expected user to be banned');
done();
});
});
it('handles a banned IP with a blank name', done => {
const user = {
getName() {
return '';
},
realip: bannedIP,
kick() {
}
};
module.onUserPreJoin(user, null, (error, res) => {
assert.equal(error, null, `Unexpected error: ${error}`);
assert.equal(res, ChannelModule.DENY, 'Expected user to be banned');
done();
});
});
it('handles a non-banned IP with a blank name', done => {
const user = {
getName() {
return '';
},
realip: '5.5.5.5'
};
module.onUserPreJoin(user, null, (error, res) => {
assert.equal(error, null, `Unexpected error: ${error}`);
assert.equal(res, ChannelModule.PASSTHROUGH, 'Expected user not to be banned');
done();
});
});
it('handles a non-banned IP with a non-banned name', done => {
const user = {
getName() {
return 'some_user';
},
realip: '5.5.5.5'
};
module.onUserPreJoin(user, null, (error, res) => {
assert.equal(error, null, `Unexpected error: ${error}`);
assert.equal(res, ChannelModule.PASSTHROUGH, 'Expected user not to be banned');
done();
});
});
});

View file

@ -0,0 +1,14 @@
const loadFromToml = require('../../lib/configuration/configloader').loadFromToml;
const path = require('path');
class IntegrationTestConfig {
constructor(config) {
this.config = config;
}
get knexConfig() {
return this.config.database;
}
}
exports.testConfig = loadFromToml(IntegrationTestConfig, path.resolve(__dirname, '..', '..', 'conf', 'integration-test.toml'));

View file

@ -0,0 +1,4 @@
const testConfig = require('./config').testConfig;
const Database = require('../../lib/database').Database;
exports.testDB = new Database(testConfig.knexConfig);

View file

@ -0,0 +1,4 @@
exports.o = function o(obj) {
// Workaround for knex returning RowDataPacket and failing assertions
return Object.assign({}, obj);
}

View file

@ -1,63 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var fs = require("fs");
function getTimeString() {
var d = new Date();
return d.toDateString() + " " + d.toTimeString().split(" ")[0];
}
var Logger = function(filename) {
this.filename = filename;
this.writer = fs.createWriteStream(filename, {
flags: "a",
encoding: "utf-8"
});
}
Logger.prototype.log = function () {
var msg = "";
for(var i in arguments)
msg += arguments[i];
if(this.dead) {
return;
}
var str = "[" + getTimeString() + "] " + msg + "\n";
try {
this.writer.write(str);
} catch(e) {
errlog.log("WARNING: Attempted logwrite failed: " + this.filename);
errlog.log("Message was: " + msg);
errlog.log(e);
}
}
Logger.prototype.close = function () {
try {
this.writer.end();
} catch(e) {
errlog.log("Log close failed: " + this.filename);
}
}
var errlog = new Logger("error.log");
var syslog = new Logger("sys.log");
errlog.actualLog = errlog.log;
errlog.log = function(what) { console.log(what); this.actualLog(what); }
syslog.actualLog = syslog.log;
syslog.log = function(what) { console.log(what); this.actualLog(what); }
exports.Logger = Logger;
exports.errlog = errlog;
exports.syslog = syslog;

View file

@ -1,95 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Helper function for formatting a time value in seconds
// to the format hh:mm:ss
function formatTime(sec) {
if(sec == "--:--")
return sec;
sec = Math.floor(sec);
var hours="", minutes="", seconds="";
if(sec > 3600) {
hours = ""+Math.floor(sec / 3600);
if(hours.length < 2)
hours = "0"+hours;
sec = sec % 3600;
}
minutes = ""+Math.floor(sec / 60);
while(minutes.length < 2) {
minutes = "0"+minutes;
}
seconds = ""+(sec % 60);
while(seconds.length < 2) {
seconds = "0"+seconds;
}
var time = "";
if(hours != "")
time = hours + ":";
time += minutes + ":" + seconds;
return time;
}
exports.formatTime = formatTime;
// Represents a media entry
var Media = function(id, title, seconds, type) {
this.id = id;
this.title = title;
this.seconds = seconds == "--:--" ? "--:--" : parseInt(seconds);
this.duration = formatTime(this.seconds);
if(seconds == "--:--") {
this.seconds = 0;
}
this.type = type;
}
Media.prototype.dup = function() {
var m = new Media(this.id, this.title, this.seconds, this.type);
return m;
}
// Returns an object containing the data in this Media but not the
// prototype
Media.prototype.pack = function() {
return {
id: this.id,
title: this.title,
seconds: this.seconds,
duration: this.duration,
type: this.type,
};
}
// Same as pack() but includes the currentTime variable set by the channel
// when the media is being synchronized
Media.prototype.fullupdate = function() {
return {
id: this.id,
title: this.title,
seconds: this.seconds,
duration: this.duration,
type: this.type,
currentTime: this.currentTime,
paused: this.paused,
};
}
Media.prototype.timeupdate = function() {
//return this.fullupdate();
return {
currentTime: this.currentTime,
paused: this.paused
};
}
exports.Media = Media;

View file

@ -1,195 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var Logger = require("./logger");
const chars = "abcdefghijklmnopqsrtuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789";
var NotWebsocket = function() {
this.hash = "";
for(var i = 0; i < 30; i++) {
this.hash += chars[parseInt(Math.random() * (chars.length - 1))];
}
this.pktqueue = [];
this.handlers = {};
this.room = "";
this.lastpoll = Date.now();
this.noflood = {};
}
NotWebsocket.prototype.checkFlood = function(id, rate) {
if(id in this.noflood) {
this.noflood[id].push(Date.now());
}
else {
this.noflood[id] = [Date.now()];
}
if(this.noflood[id].length > 10) {
this.noflood[id].shift();
var hz = 10000 / (this.noflood[id][9] - this.noflood[id][0]);
if(hz > rate) {
throw "Rate is too high: " + id;
}
}
}
NotWebsocket.prototype.emit = function(msg, data) {
var pkt = [msg, data];
this.pktqueue.push(pkt);
}
NotWebsocket.prototype.poll = function() {
this.checkFlood("poll", 100);
this.lastpoll = Date.now();
var q = [];
for(var i = 0; i < this.pktqueue.length; i++) {
q.push(this.pktqueue[i]);
}
this.pktqueue.length = 0;
return q;
}
NotWebsocket.prototype.on = function(msg, callback) {
if(!(msg in this.handlers))
this.handlers[msg] = [];
this.handlers[msg].push(callback);
}
NotWebsocket.prototype.recv = function(urlstr) {
this.checkFlood("recv", 100);
var msg, data;
try {
var js = JSON.parse(urlstr);
msg = js[0];
data = js[1];
}
catch(e) {
Logger.errlog.log("Failed to parse NWS string");
Logger.errlog.log(urlstr);
}
if(!msg)
return;
if(!(msg in this.handlers))
return;
for(var i = 0; i < this.handlers[msg].length; i++) {
this.handlers[msg][i](data);
}
}
NotWebsocket.prototype.join = function(rm) {
if(!(rm in rooms)) {
rooms[rm] = [];
}
rooms[rm].push(this);
}
NotWebsocket.prototype.leave = function(rm) {
if(rm in rooms) {
var idx = rooms[rm].indexOf(this);
if(idx >= 0) {
rooms[rm].splice(idx, 1);
}
}
}
NotWebsocket.prototype.disconnect = function() {
for(var rm in rooms) {
this.leave(rm);
}
this.recv(JSON.stringify(["disconnect", undefined]));
this.emit("disconnect");
clients[this.hash] = null;
delete clients[this.hash];
}
function sendJSON(res, obj) {
var response = JSON.stringify(obj, null, 4);
if(res.callback) {
response = res.callback + "(" + response + ")";
}
var len = unescape(encodeURIComponent(response)).length;
res.setHeader("Content-Type", "application/json");
res.setHeader("Content-Length", len);
res.end(response);
}
var clients = {};
var rooms = {};
function newConnection(req, res) {
var nws = new NotWebsocket();
clients[nws.hash] = nws;
res.callback = req.query.callback;
sendJSON(res, nws.hash);
return nws;
}
exports.newConnection = newConnection;
function msgReceived(req, res) {
res.callback = req.query.callback;
var h = req.params.hash;
if(h in clients && clients[h] != null) {
var str = req.params.str;
res.callback = req.query.callback;
try {
if(str == "poll") {
sendJSON(res, clients[h].poll());
}
else {
clients[h].recv(decodeURIComponent(str));
sendJSON(res, "");
}
}
catch(e) {
res.send(429); // 429 Too Many Requests
}
}
else {
res.send(404);
}
}
exports.msgReceived = msgReceived;
function inRoom(rm) {
var cl = [];
if(rm in rooms) {
for(var i = 0; i < rooms[rm].length; i++) {
cl.push(rooms[rm][i]);
}
}
cl.emit = function(msg, data) {
for(var i = 0; i < this.length; i++) {
this[i].emit(msg, data);
}
};
return cl;
}
exports.inRoom = inRoom;
function checkDeadSockets() {
for(var h in clients) {
if(Date.now() - clients[h].lastpoll >= 2000) {
clients[h].disconnect();
}
}
}
setInterval(checkDeadSockets, 2000);

7481
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,82 @@
{
"author": "Calvin Montgomery",
"name": "CyTube",
"description": "Online media synchronizer and chat",
"version": "2.3.1",
"repository": {
"url": "http://github.com/calzoneman/sync"
},
"dependencies": {
"socket.io": ">=0.9",
"express": ">=3.2",
"mysql-libmysqlclient": "*",
"node_hash": "*",
"bcrypt": "*",
"nodemailer": "*",
"validator": "*"
}
"author": "Calvin Montgomery",
"name": "CyTube",
"description": "Online media synchronizer and chat",
"version": "3.86.1",
"repository": {
"url": "http://github.com/calzoneman/sync"
},
"license": "MIT",
"dependencies": {
"@calzoneman/jsli": "^2.0.1",
"@cytube/mediaquery": "github:CyTube/mediaquery#52a635d45f38785bdb26e842c9f2bb505bc37a1c",
"bcrypt": "^5.0.1",
"bluebird": "^3.7.2",
"body-parser": "^1.20.1",
"cheerio": "^1.0.0-rc.10",
"clone": "^2.1.2",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
"create-error": "^0.3.1",
"csrf": "^3.1.0",
"cytubefilters": "github:calzoneman/cytubefilters#c67b2dab2dc5cc5ed11018819f71273d0f8a1bf5",
"express": "^4.18.2",
"express-minify": "^1.0.0",
"json-typecheck": "^0.1.3",
"knex": "^2.4.0",
"lodash": "^4.17.21",
"morgan": "^1.10.0",
"mysql": "^2.18.1",
"nodemailer": "^6.6.1",
"prom-client": "^13.1.0",
"proxy-addr": "^2.0.6",
"pug": "^3.0.2",
"redis": "^3.1.1",
"sanitize-html": "^2.7.0",
"serve-static": "^1.15.0",
"socket.io": "^4.5.4",
"source-map-support": "^0.5.19",
"toml": "^3.0.0",
"uuid": "^8.3.2",
"yamljs": "^0.2.8"
},
"scripts": {
"build-player": "./bin/build-player.js",
"build-server": "babel -D --source-maps --out-dir lib/ src/",
"flow": "flow",
"lint": "eslint src",
"pretest": "npm run lint",
"postinstall": "./postinstall.sh",
"server-dev": "babel -D --watch --source-maps --verbose --out-dir lib/ src/",
"generate-userscript": "$npm_node_execpath gdrive-userscript/generate-userscript $@ > www/js/cytube-google-drive.user.js",
"test": "mocha --recursive --exit test",
"integration-test": "mocha --recursive --exit integration_test"
},
"devDependencies": {
"@babel/cli": "^7.15.7",
"@babel/core": "^7.15.8",
"@babel/eslint-parser": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"babel-plugin-add-module-exports": "^1.0.4",
"coffeescript": "^1.9.2",
"eslint": "^7.32.0",
"eslint-plugin-no-jquery": "^2.7.0",
"mocha": "^9.2.2",
"sinon": "^10.0.0"
},
"babel": {
"presets": [
[
"@babel/env",
{
"targets": {
"node": "12"
}
}
]
],
"plugins": [
"add-module-exports"
]
}
}

36
player/base.coffee Normal file
View file

@ -0,0 +1,36 @@
window.Player = class Player
constructor: (data) ->
if not (this instanceof Player)
return new Player(data)
@setMediaProperties(data)
@paused = false
load: (data) ->
@setMediaProperties(data)
setMediaProperties: (data) ->
@mediaId = data.id
@mediaType = data.type
@mediaLength = data.seconds
play: ->
@paused = false
pause: ->
@paused = true
seekTo: (time) ->
setVolume: (volume) ->
getTime: (cb) ->
cb(0)
isPaused: (cb) ->
cb(@paused)
getVolume: (cb) ->
cb(VOLUME)
destroy: ->

View file

@ -0,0 +1,39 @@
CUSTOM_EMBED_WARNING = 'This channel is embedding custom content from %link%.
Since this content is not trusted, you must click "Embed" below to allow
the content to be embedded.<hr>'
window.CustomEmbedPlayer = class CustomEmbedPlayer extends EmbedPlayer
constructor: (data) ->
if not (this instanceof CustomEmbedPlayer)
return new CustomEmbedPlayer(data)
@load(data)
load: (data) ->
if not data.meta.embed?
console.error('CustomEmbedPlayer::load(): missing meta.embed')
return
embedSrc = data.meta.embed.src
link = document.createElement('a')
link.href = embedSrc
link.target = '_blank'
link.rel = 'noopener noreferer'
strong = document.createElement('strong')
strong.textContent = embedSrc
link.appendChild(strong)
# TODO: Ideally makeAlert() would allow optionally providing a DOM
# element instead of requiring HTML text
alert = makeAlert('Untrusted Content', CUSTOM_EMBED_WARNING.replace('%link%', link.outerHTML),
'alert-warning')
.removeClass('col-md-12')
$('<button/>').addClass('btn btn-default')
.text('Embed')
.on('click', =>
super(data)
)
.appendTo(alert.find('.alert'))
removeOld(alert)

135
player/dailymotion.coffee Normal file
View file

@ -0,0 +1,135 @@
window.DailymotionPlayer = class DailymotionPlayer extends Player
constructor: (data) ->
if not (this instanceof DailymotionPlayer)
return new DailymotionPlayer(data)
@setMediaProperties(data)
@initialVolumeSet = false
@playbackReadyCb = null
waitUntilDefined(window, 'DM', =>
removeOld()
params =
autoplay: 1
logo: 0
quality = @mapQuality(USEROPTS.default_quality)
if quality != 'auto'
params.quality = quality
@element = DM.$('ytapiplayer')
if not @element or @element.nodeType != Node.ELEMENT_NODE
throw new Error("Invalid player element in DailymotionPlayer(), requires an existing HTML element: " + @element)
if DM.Player._INSTANCES[@element.id] != undefined
@element = DM.Player.destroy(@element.id)
@dm = DM.Player.create(@element,
video: data.id
width: parseInt(VWIDTH, 10)
height: parseInt(VHEIGHT, 10)
params: params
)
@dm.addEventListener('apiready', =>
@dmReady = true
@dm.addEventListener('ended', ->
if CLIENT.leader
socket.emit('playNext')
)
@dm.addEventListener('pause', =>
@paused = true
if CLIENT.leader
sendVideoUpdate()
)
@dm.addEventListener('playing', =>
@paused = false
if CLIENT.leader
sendVideoUpdate()
if not @initialVolumeSet
@setVolume(VOLUME)
@initialVolumeSet = true
)
# Once the video stops, the internal state of the player
# becomes unusable and attempting to load() will corrupt it and
# crash the player with an error. As a shortmedium term
# workaround, mark the player as "not ready" until the next
# playback_ready event
@dm.addEventListener('video_end', =>
@dmReady = false
)
@dm.addEventListener('playback_ready', =>
@dmReady = true
if @playbackReadyCb
@playbackReadyCb()
@playbackReadyCb = null
)
)
)
load: (data) ->
@setMediaProperties(data)
if @dm and @dmReady
@dm.load(data.id)
@dm.seek(data.currentTime)
else if @dm
# TODO: Player::load() needs to be made asynchronous in the future
console.log('Warning: load() called before DM is ready, queueing callback')
@playbackReadyCb = () =>
@dm.load(data.id)
@dm.seek(data.currentTime)
else
console.error('WTF? DailymotionPlayer::load() called but @dm is undefined')
pause: ->
if @dm and @dmReady
@paused = true
@dm.pause()
play: ->
if @dm and @dmReady
@paused = false
@dm.play()
seekTo: (time) ->
if @dm and @dmReady
@dm.seek(time)
setVolume: (volume) ->
if @dm and @dmReady
@dm.setVolume(volume)
getTime: (cb) ->
if @dm and @dmReady
cb(@dm.currentTime)
else
cb(0)
getVolume: (cb) ->
if @dm and @dmReady
if @dm.muted
cb(0)
else
volume = @dm.volume
# There was once a bug in Dailymotion where it sometimes gave back
# volumes in the wrong range. Not sure if this is still a necessary
# check.
if volume > 1
volume /= 100
cb(volume)
else
cb(VOLUME)
mapQuality: (quality) ->
switch String(quality)
when '240', '480', '720', '1080' then String(quality)
when '360' then '380'
when 'best' then '1080'
else 'auto'
destroy: ->
if @dm
@dm.destroy('ytapiplayer')

49
player/embed.coffee Normal file
View file

@ -0,0 +1,49 @@
DEFAULT_ERROR = 'You are currently connected via HTTPS but the embedded content
uses non-secure plain HTTP. Your browser therefore blocks it from
loading due to mixed content policy. To fix this, embed the video using a
secure link if available (https://...), or find another source for the content.'
genParam = (name, value) ->
$('<param/>').attr(
name: name
value: value
)
window.EmbedPlayer = class EmbedPlayer extends Player
constructor: (data) ->
if not (this instanceof EmbedPlayer)
return new EmbedPlayer(data)
@load(data)
load: (data) ->
@setMediaProperties(data)
embed = data.meta.embed
if not embed?
console.error('EmbedPlayer::load(): missing meta.embed')
return
@player = @loadIframe(embed)
removeOld(@player)
loadIframe: (embed) ->
if embed.src.indexOf('http:') == 0 and location.protocol == 'https:'
if @__proto__.mixedContentError?
error = @__proto__.mixedContentError
else
error = DEFAULT_ERROR
alert = makeAlert('Mixed Content Error', error, 'alert-danger')
.removeClass('col-md-12')
alert.find('.close').remove()
return alert
else
iframe = $('<iframe/>').attr(
src: embed.src
frameborder: '0'
allow: 'autoplay'
allowfullscreen: '1'
)
return iframe

View file

@ -0,0 +1,86 @@
window.GoogleDrivePlayer = class GoogleDrivePlayer extends VideoJSPlayer
constructor: (data) ->
if not (this instanceof GoogleDrivePlayer)
return new GoogleDrivePlayer(data)
super(data)
load: (data) ->
if not window.hasDriveUserscript
window.promptToInstallDriveUserscript()
else if window.hasDriveUserscript
window.maybePromptToUpgradeUserscript()
if typeof window.getGoogleDriveMetadata is 'function'
setTimeout(=>
backoffRetry((cb) ->
window.getGoogleDriveMetadata(data.id, cb)
, (error, metadata) =>
if error
console.error(error)
alertBox = window.document.createElement('div')
alertBox.className = 'alert alert-danger'
alertBox.textContent = error
document.getElementById('ytapiplayer').appendChild(alertBox)
else
data.meta.direct = metadata.videoMap
super(data)
, {
maxTries: 3
delay: 1000
factor: 1.2
jitter: 500
})
, Math.random() * 1000)
window.promptToInstallDriveUserscript = ->
if document.getElementById('prompt-install-drive-userscript')
return
alertBox = document.createElement('div')
alertBox.id = 'prompt-install-drive-userscript'
alertBox.className = 'alert alert-info'
alertBox.innerHTML = """
Due to continual breaking changes making it increasingly difficult to
maintain Google Drive support, Google Drive now requires installing
a userscript in order to play the video."""
alertBox.appendChild(document.createElement('br'))
infoLink = document.createElement('a')
infoLink.className = 'btn btn-info'
infoLink.href = '/google_drive_userscript'
infoLink.textContent = 'Click here for details'
infoLink.target = '_blank'
alertBox.appendChild(infoLink)
closeButton = document.createElement('button')
closeButton.className = 'close pull-right'
closeButton.innerHTML = '&times;'
closeButton.onclick = ->
alertBox.parentNode.removeChild(alertBox)
alertBox.insertBefore(closeButton, alertBox.firstChild)
removeOld($('<div/>').append(alertBox))
window.tellUserNotToContactMeAboutThingsThatAreNotSupported = ->
if document.getElementById('prompt-no-gdrive-support')
return
alertBox = document.createElement('div')
alertBox.id = 'prompt-no-gdrive-support'
alertBox.className = 'alert alert-danger'
alertBox.innerHTML = """
CyTube has detected an error in Google Drive playback. Please note that the
staff in CyTube support channels DO NOT PROVIDE SUPPORT FOR GOOGLE DRIVE. It
is left in the code as-is for existing users, but we will not assist in
troubleshooting any errors that occur.<br>"""
alertBox.appendChild(document.createElement('br'))
infoLink = document.createElement('a')
infoLink.className = 'btn btn-danger'
infoLink.href = 'https://github.com/calzoneman/sync/wiki/Frequently-Asked-Questions#why-dont-you-support-google-drive-anymore'
infoLink.textContent = 'Click here for details'
infoLink.target = '_blank'
alertBox.appendChild(infoLink)
closeButton = document.createElement('button')
closeButton.className = 'close pull-right'
closeButton.innerHTML = '&times;'
closeButton.onclick = ->
alertBox.parentNode.removeChild(alertBox)
alertBox.insertBefore(closeButton, alertBox.firstChild)
removeOld($('<div/>').append(alertBox))

23
player/hls.coffee Normal file
View file

@ -0,0 +1,23 @@
window.HLSPlayer = class HLSPlayer extends VideoJSPlayer
constructor: (data) ->
if not (this instanceof HLSPlayer)
return new HLSPlayer(data)
@setupMeta(data)
super(data)
load: (data) ->
@setupMeta(data)
super(data)
setupMeta: (data) ->
data.meta.direct =
# Quality is required for data.meta.direct processing but doesn't
# matter here because it's dictated by the stream. Arbitrarily
# choose 480.
480: [
{
link: data.id
contentType: 'application/x-mpegURL'
}
]

33
player/iframechild.coffee Normal file
View file

@ -0,0 +1,33 @@
window.IframeChild = class IframeChild extends PlayerJSPlayer
constructor: (data) ->
if not (this instanceof IframeChild)
return new IframeChild(data)
super(data)
load: (data) ->
@setMediaProperties(data)
@ready = false
waitUntilDefined(window, 'playerjs', =>
iframe = $('<iframe/>')
.attr(
src: '/iframe'
allow: 'autoplay; fullscreen'
)
removeOld(iframe)
@setupFrame(iframe[0], data)
@setupPlayer(iframe[0])
)
setupFrame: (iframe, data) ->
iframe.addEventListener('load', =>
# TODO: ideally, communication with the child frame should use postMessage()
iframe.contentWindow.VOLUME = VOLUME
iframe.contentWindow.loadMediaPlayer(Object.assign({}, data, { type: 'cm' } ))
iframe.contentWindow.document.querySelector('#ytapiplayer').classList.add('vjs-16-9')
adapter = iframe.contentWindow.playerjs.VideoJSAdapter(iframe.contentWindow.PLAYER.player)
adapter.ready()
typeof data?.meta?.thumbnail == 'string' and iframe.contentWindow.PLAYER.player.poster(data.meta.thumbnail)
)

View file

@ -0,0 +1,17 @@
window.LivestreamPlayer = class LivestreamPlayer extends EmbedPlayer
constructor: (data) ->
if not (this instanceof LivestreamPlayer)
return new LivestreamPlayer(data)
@load(data)
load: (data) ->
[ account, event ] = data.id.split(';')
data.meta.embed =
src: "https://livestream.com/accounts/#{account}/events/#{event}/player?\
enableInfoAndActivity=false&\
defaultDrawer=&\
autoPlay=true&\
mute=false"
tag: 'iframe'
super(data)

66
player/niconico.coffee Normal file
View file

@ -0,0 +1,66 @@
window.NicoPlayer = class NicoPlayer extends Player
constructor: (data) ->
if not (this instanceof NicoPlayer)
return new NicoPlayer(data)
@load(data)
load: (data) ->
@setMediaProperties(data)
waitUntilDefined(window, 'NicovideoEmbed', =>
@nico = new NicovideoEmbed({ playerId: 'ytapiplayer', videoId: data.id })
removeOld($(@nico.iframe))
@nico.on('ended', =>
if CLIENT.leader
socket.emit('playNext')
)
@nico.on('pause', =>
@paused = true
if CLIENT.leader
sendVideoUpdate()
)
@nico.on('play', =>
@paused = false
if CLIENT.leader
sendVideoUpdate()
)
@nico.on('ready', =>
@play()
@setVolume(VOLUME)
)
)
play: ->
@paused = false
if @nico
@nico.play()
pause: ->
@paused = true
if @nico
@nico.pause()
seekTo: (time) ->
if @nico
@nico.seek(time * 1000)
setVolume: (volume) ->
if @nico
@nico.volumeChange(volume)
getTime: (cb) ->
if @nico
cb(parseFloat(@nico.state.currentTime / 1000))
else
cb(0)
getVolume: (cb) ->
if @nico
cb(parseFloat(@nico.state.volume))
else
cb(VOLUME)

21
player/odysee.coffee Normal file
View file

@ -0,0 +1,21 @@
window.OdyseePlayer = class OdyseePlayer extends PlayerJSPlayer
constructor: (data) ->
if not (this instanceof OdyseePlayer)
return new OdyseePlayer(data)
super(data)
load: (data) ->
@ready = false
@setMediaProperties(data)
waitUntilDefined(window, 'playerjs', =>
iframe = $('<iframe/>')
.attr(
src: data.meta.embed.src
allow: 'autoplay; fullscreen'
)
removeOld(iframe)
@setupPlayer(iframe[0], data)
)

122
player/peertube.coffee Normal file
View file

@ -0,0 +1,122 @@
PEERTUBE_EMBED_WARNING = 'This channel is embedding PeerTube content from %link%.
PeerTube instances may use P2P technology that will expose your IP address to third parties, including but not
limited to other users in this channel. It is also conceivable that if the content in question is in violation of
copyright laws your IP address could be potentially be observed by legal authorities monitoring the tracker of
this PeerTube instance. The operators of %site% are not responsible for the data sent by the embedded player to
third parties on your behalf.<br><br> If you understand the risks, wish to assume all liability, and continue to
the content, click "Embed" below to allow the content to be embedded.<hr>'
PEERTUBE_RISK = false
window.PeerPlayer = class PeerPlayer extends Player
constructor: (data) ->
if not (this instanceof PeerPlayer)
return new PeerPlayer(data)
@warn(data)
warn: (data) ->
if USEROPTS.peertube_risk or PEERTUBE_RISK
return @load(data)
site = new URL(document.URL).hostname
embedSrc = data.meta.embed.domain
link = "<a href=\"http://#{embedSrc}\" target=\"_blank\" rel=\"noopener noreferer\"><strong>#{embedSrc}</strong></a>"
alert = makeAlert('Privacy Advisory', PEERTUBE_EMBED_WARNING.replace('%link%', link).replace('%site%', site),
'alert-warning')
.removeClass('col-md-12')
$('<button/>').addClass('btn btn-default')
.text('Embed')
.on('click', =>
@load(data)
)
.appendTo(alert.find('.alert'))
$('<button/>').addClass('btn btn-default pull-right')
.text('Embed and dont ask again for this session')
.on('click', =>
PEERTUBE_RISK = true
@load(data)
)
.appendTo(alert.find('.alert'))
removeOld(alert)
load: (data) ->
@setMediaProperties(data)
waitUntilDefined(window, 'PeerTubePlayer', =>
video = $('<iframe/>')
removeOld(video)
video.attr(
src: "https://#{data.meta.embed.domain}/videos/embed/#{data.meta.embed.uuid}?api=1"
allow: 'autoplay; fullscreen'
)
@peertube = new PeerTubePlayer(video[0])
@peertube.addEventListener('playbackStatusChange', (status) =>
@paused = status == 'paused'
if CLIENT.leader
sendVideoUpdate()
)
@peertube.addEventListener('playbackStatusUpdate', (status) =>
@peertube.currentTime = status.position
if status.playbackState == "ended" and CLIENT.leader
socket.emit('playNext')
)
@peertube.addEventListener('volumeChange', (volume) =>
VOLUME = volume
setOpt("volume", VOLUME)
)
@play()
@setVolume(VOLUME)
)
play: ->
@paused = false
if @peertube and @peertube.ready
@peertube.play().catch((error) ->
console.error('PeerTube::play():', error)
)
pause: ->
@paused = true
if @peertube and @peertube.ready
@peertube.pause().catch((error) ->
console.error('PeerTube::pause():', error)
)
seekTo: (time) ->
if @peertube and @peertube.ready
@peertube.seek(time)
getVolume: (cb) ->
if @peertube and @peertube.ready
@peertube.getVolume().then((volume) ->
cb(parseFloat(volume))
).catch((error) ->
console.error('PeerTube::getVolume():', error)
)
else
cb(VOLUME)
setVolume: (volume) ->
if @peertube and @peertube.ready
@peertube.setVolume(volume).catch((error) ->
console.error('PeerTube::setVolume():', error)
)
getTime: (cb) ->
if @peertube and @peertube.ready
cb(@peertube.currentTime)
else
cb(0)
setQuality: (quality) ->
# USEROPTS.default_quality
# @peertube.getResolutions()
# @peertube.setResolution(resolutionId : number)

85
player/playerjs.coffee Normal file
View file

@ -0,0 +1,85 @@
window.PlayerJSPlayer = class PlayerJSPlayer extends Player
constructor: (data) ->
if not (this instanceof PlayerJSPlayer)
return new PlayerJSPlayer(data)
@load(data)
load: (data) ->
@setMediaProperties(data)
@ready = false
if not data.meta.playerjs
throw new Error('Invalid input: missing meta.playerjs')
waitUntilDefined(window, 'playerjs', =>
iframe = $('<iframe/>')
.attr(
src: data.meta.playerjs.src
allow: 'autoplay; fullscreen'
)
removeOld(iframe)
@setupPlayer(iframe[0])
)
setupPlayer: (iframe) ->
@player = new playerjs.Player(iframe)
@player.on('ready', =>
@player.on('error', (error) =>
console.error('PlayerJS error', error.stack)
)
@player.on('ended', ->
if CLIENT.leader
socket.emit('playNext')
)
@player.on('play', ->
@paused = false
if CLIENT.leader
sendVideoUpdate()
)
@player.on('pause', ->
@paused = true
if CLIENT.leader
sendVideoUpdate()
)
@player.setVolume(VOLUME * 100)
if not @paused
@player.play()
@ready = true
)
play: ->
@paused = false
if @player and @ready
@player.play()
pause: ->
@paused = true
if @player and @ready
@player.pause()
seekTo: (time) ->
if @player and @ready
@player.setCurrentTime(time)
setVolume: (volume) ->
if @player and @ready
@player.setVolume(volume * 100)
getTime: (cb) ->
if @player and @ready
@player.getCurrentTime(cb)
else
cb(0)
getVolume: (cb) ->
if @player and @ready
@player.getVolume((volume) ->
cb(volume / 100)
)
else
cb(VOLUME)

31
player/raw-file.coffee Normal file
View file

@ -0,0 +1,31 @@
codecToMimeType = (codec) ->
switch codec
when 'mov/h264', 'mov/av1' then 'video/mp4'
when 'flv/h264' then 'video/flv'
when 'matroska/vp8', 'matroska/vp9', 'matroska/av1' then 'video/webm'
when 'ogg/theora' then 'video/ogg'
when 'mp3' then 'audio/mp3'
when 'vorbis' then 'audio/ogg'
when 'aac' then 'audio/aac'
when 'opus' then 'audio/opus'
else 'video/flv'
window.FilePlayer = class FilePlayer extends VideoJSPlayer
constructor: (data) ->
if not (this instanceof FilePlayer)
return new FilePlayer(data)
data.meta.direct =
480: [{
contentType: codecToMimeType(data.meta.codec)
link: data.id
}]
super(data)
load: (data) ->
data.meta.direct =
480: [{
contentType: codecToMimeType(data.meta.codec)
link: data.id
}]
super(data)

23
player/rtmp.coffee Normal file
View file

@ -0,0 +1,23 @@
window.RTMPPlayer = class RTMPPlayer extends VideoJSPlayer
constructor: (data) ->
if not (this instanceof RTMPPlayer)
return new RTMPPlayer(data)
@setupMeta(data)
super(data)
load: (data) ->
@setupMeta(data)
super(data)
setupMeta: (data) ->
data.meta.direct =
# Quality is required for data.meta.direct processing but doesn't
# matter here because it's dictated by the stream. Arbitrarily
# choose 480.
480: [
{
link: data.id
contentType: 'rtmp/flv'
}
]

108
player/soundcloud.coffee Normal file
View file

@ -0,0 +1,108 @@
window.SoundCloudPlayer = class SoundCloudPlayer extends Player
constructor: (data) ->
if not (this instanceof SoundCloudPlayer)
return new SoundCloudPlayer(data)
@setMediaProperties(data)
waitUntilDefined(window, 'SC', =>
removeOld()
# For tracks that are private, but embeddable, the API returns a
# special URL to load into the player.
# TODO: rename scuri?
if data.meta.scuri
soundUrl = data.meta.scuri
else
soundUrl = data.id
widget = $('<iframe/>').appendTo($('#ytapiplayer'))
widget.attr(
id: 'scplayer'
src: "https://w.soundcloud.com/player/?url=#{soundUrl}"
)
# Soundcloud embed widget doesn't have a volume control.
sliderHolder = $('<div/>').attr('id', 'soundcloud-volume-holder')
.insertAfter(widget)
$('<span/>').attr('id', 'soundcloud-volume-label')
.addClass('label label-default')
.text('Volume')
.appendTo(sliderHolder)
volumeSlider = $('<div/>').attr('id', 'soundcloud-volume')
.appendTo(sliderHolder)
.slider(
range: 'min'
value: VOLUME * 100
stop: (event, ui) =>
@setVolume(ui.value / 100)
)
@soundcloud = SC.Widget(widget[0])
@soundcloud.bind(SC.Widget.Events.READY, =>
@soundcloud.ready = true
@setVolume(VOLUME)
@play()
@soundcloud.bind(SC.Widget.Events.PAUSE, =>
@paused = true
if CLIENT.leader
sendVideoUpdate()
)
@soundcloud.bind(SC.Widget.Events.PLAY, =>
@paused = false
if CLIENT.leader
sendVideoUpdate()
)
@soundcloud.bind(SC.Widget.Events.FINISH, =>
if CLIENT.leader
socket.emit('playNext')
)
)
)
load: (data) ->
@setMediaProperties(data)
if @soundcloud and @soundcloud.ready
if data.meta.scuri
soundUrl = data.meta.scuri
else
soundUrl = data.id
@soundcloud.load(soundUrl, auto_play: true)
@soundcloud.bind(SC.Widget.Events.READY, =>
@setVolume(VOLUME)
)
else
console.error('SoundCloudPlayer::load() called but soundcloud is not ready')
play: ->
@paused = false
if @soundcloud and @soundcloud.ready
@soundcloud.play()
pause: ->
@paused = true
if @soundcloud and @soundcloud.ready
@soundcloud.pause()
seekTo: (time) ->
if @soundcloud and @soundcloud.ready
# SoundCloud measures time in milliseconds while CyTube uses seconds.
@soundcloud.seekTo(time * 1000)
setVolume: (volume) ->
if @soundcloud and @soundcloud.ready
@soundcloud.setVolume(volume * 100)
getTime: (cb) ->
if @soundcloud and @soundcloud.ready
# Returned time is in milliseconds; CyTube expects seconds
@soundcloud.getPosition((time) -> cb(time / 1000))
else
cb(0)
getVolume: (cb) ->
if @soundcloud and @soundcloud.ready
@soundcloud.getVolume((vol) -> cb(vol / 100))
else
cb(VOLUME)

35
player/streamable.coffee Normal file
View file

@ -0,0 +1,35 @@
window.StreamablePlayer = class StreamablePlayer extends PlayerJSPlayer
constructor: (data) ->
if not (this instanceof StreamablePlayer)
return new StreamablePlayer(data)
super(data)
load: (data) ->
@ready = false
@finishing = false
@setMediaProperties(data)
waitUntilDefined(window, 'playerjs', =>
iframe = $('<iframe/>')
.attr(
src: "https://streamable.com/e/#{data.id}"
allow: 'autoplay; fullscreen'
)
removeOld(iframe)
@setupPlayer(iframe[0])
@player.on('ready', =>
# Streamable does not implement ended event since it loops
# gotta use a timeupdate hack
@player.on('timeupdate', (time) =>
if time.duration - time.seconds < 1 and not @finishing
setTimeout(=>
if CLIENT.leader
socket.emit('playNext')
@pause()
, (time.duration - time.seconds) * 1000)
@finishing = true
)
)
)

128
player/twitch.coffee Normal file
View file

@ -0,0 +1,128 @@
window.TWITCH_PARAMS_ERROR = 'The Twitch embed player now uses parameters which only
work if the following requirements are met: (1) The embedding website uses
HTTPS; (2) The embedding website uses the default port (443) and is accessed
via https://example.com instead of https://example.com:port. I have no
control over this -- see <a href="https://discuss.dev.twitch.tv/t/twitch-embedded-player-migration-timeline-update/25588" rel="noopener noreferrer" target="_blank">this Twitch post</a>
for details'
window.TwitchPlayer = class TwitchPlayer extends Player
constructor: (data) ->
if not (this instanceof TwitchPlayer)
return new TwitchPlayer(data)
@setMediaProperties(data)
waitUntilDefined(window, 'Twitch', =>
waitUntilDefined(Twitch, 'Player', =>
@init(data)
)
)
init: (data) ->
removeOld()
if location.hostname != location.host or location.protocol != 'https:'
alert = makeAlert(
'Twitch API Parameters',
window.TWITCH_PARAMS_ERROR,
'alert-danger'
).removeClass('col-md-12')
removeOld(alert)
@twitch = null
return
options =
parent: [location.hostname]
width: $('#ytapiplayer').width()
height: $('#ytapiplayer').height()
if data.type is 'tv'
# VOD
options.video = data.id
else
# Livestream
options.channel = data.id
@twitch = new Twitch.Player('ytapiplayer', options)
@twitch.addEventListener(Twitch.Player.READY, =>
@setVolume(VOLUME)
@twitch.setQuality(@mapQuality(USEROPTS.default_quality))
@twitch.addEventListener(Twitch.Player.PLAY, =>
@paused = false
if CLIENT.leader
sendVideoUpdate()
)
@twitch.addEventListener(Twitch.Player.PAUSE, =>
@paused = true
if CLIENT.leader
sendVideoUpdate()
)
@twitch.addEventListener(Twitch.Player.ENDED, =>
if CLIENT.leader
socket.emit('playNext')
)
)
load: (data) ->
@setMediaProperties(data)
try
if data.type is 'tv'
# VOD
@twitch.setVideo(data.id)
else
# Livestream
@twitch.setChannel(data.id)
catch error
console.error(error)
pause: ->
try
@twitch.pause()
@paused = true
catch error
console.error(error)
play: ->
try
@twitch.play()
@paused = false
catch error
console.error(error)
seekTo: (time) ->
try
@twitch.seek(time)
catch error
console.error(error)
getTime: (cb) ->
try
cb(@twitch.getCurrentTime())
catch error
console.error(error)
setVolume: (volume) ->
try
@twitch.setVolume(volume)
if volume > 0
@twitch.setMuted(false)
catch error
console.error(error)
getVolume: (cb) ->
try
if @twitch.isPaused()
cb(0)
else
cb(@twitch.getVolume())
catch error
console.error(error)
mapQuality: (quality) ->
switch String(quality)
when '1080' then 'chunked'
when '720' then 'high'
when '480' then 'medium'
when '360' then 'low'
when '240' then 'mobile'
when 'best' then 'chunked'
else ''

21
player/twitchclip.coffee Normal file
View file

@ -0,0 +1,21 @@
window.TwitchClipPlayer = class TwitchClipPlayer extends EmbedPlayer
constructor: (data) ->
if not (this instanceof TwitchClipPlayer)
return new TwitchClipPlayer(data)
@load(data)
load: (data) ->
if location.hostname != location.host or location.protocol != 'https:'
alert = makeAlert(
'Twitch API Parameters',
window.TWITCH_PARAMS_ERROR,
'alert-danger'
).removeClass('col-md-12')
removeOld(alert)
return
data.meta.embed =
tag: 'iframe'
src: "https://clips.twitch.tv/embed?clip=#{data.id}&parent=#{location.host}"
super(data)

118
player/update.coffee Normal file
View file

@ -0,0 +1,118 @@
TYPE_MAP =
yt: YouTubePlayer
vi: VimeoPlayer
dm: DailymotionPlayer
gd: GoogleDrivePlayer
fi: FilePlayer
sc: SoundCloudPlayer
li: LivestreamPlayer
tw: TwitchPlayer
tv: TwitchPlayer
cu: CustomEmbedPlayer
rt: RTMPPlayer
hl: HLSPlayer
sb: StreamablePlayer
tc: TwitchClipPlayer
cm: VideoJSPlayer
pt: PeerPlayer
bc: IframeChild
bn: IframeChild
od: OdyseePlayer
wp: WhepPlayer
nv: NicoPlayer
window.loadMediaPlayer = (data) ->
try
if window.PLAYER
window.PLAYER.destroy()
catch error
console.error error
if data.meta.direct and data.type is 'vi'
try
window.PLAYER = new VideoJSPlayer(data)
catch e
console.error e
else if data.type of TYPE_MAP
try
window.PLAYER = TYPE_MAP[data.type](data)
catch e
console.error e
window.handleMediaUpdate = (data) ->
PLAYER = window.PLAYER
# Do not update if the current time is past the end of the video, unless
# the video has length 0 (which is a special case for livestreams)
if typeof PLAYER.mediaLength is 'number' and
PLAYER.mediaLength > 0 and
data.currentTime > PLAYER.mediaLength
return
# Negative currentTime indicates a lead-in for clients to load the video,
# but not play it yet (helps with initial buffering)
waiting = data.currentTime < 0
# Load a new video in the same player if the ID changed
if data.id and data.id != PLAYER.mediaId
if data.currentTime < 0
data.currentTime = 0
PLAYER.load(data)
PLAYER.play()
if waiting
PLAYER.seekTo(0)
# YouTube player has a race condition that crashes the player if
# play(), seek(0), and pause() are called quickly without waiting
# for events to fire. Setting a flag variable that is checked in the
# event handler mitigates this.
if PLAYER instanceof YouTubePlayer
PLAYER.pauseSeekRaceCondition = true
else
PLAYER.pause()
return
else if PLAYER instanceof YouTubePlayer
PLAYER.pauseSeekRaceCondition = false
if CLIENT.leader or not USEROPTS.synch
return
if data.paused and not PLAYER.paused
PLAYER.seekTo(data.currentTime)
PLAYER.pause()
else if PLAYER.paused and not data.paused
PLAYER.play()
PLAYER.getTime((seconds) ->
time = data.currentTime
diff = (time - seconds) or time
accuracy = USEROPTS.sync_accuracy
# Dailymotion can't seek very accurately in Flash due to keyframe
# placement. Accuracy should not be set lower than 5 or the video
# may be very choppy.
if PLAYER instanceof DailymotionPlayer
accuracy = Math.max(accuracy, 5)
if diff > accuracy
# The player is behind the correct time
PLAYER.seekTo(time)
else if diff < -accuracy
# The player is ahead of the correct time
# Don't seek all the way back, to account for possible buffering.
# However, do seek all the way back for Dailymotion due to the
# keyframe issue mentioned above.
if not (PLAYER instanceof DailymotionPlayer)
time += 1
PLAYER.seekTo(time)
)
window.removeOld = (replace) ->
$('#soundcloud-volume-holder').remove()
replace ?= $('<div/>').addClass('embed-responsive-item')
old = $('#ytapiplayer')
old.attr('id', 'ytapiplayer-old')
replace.attr('id', 'ytapiplayer')
replace.insertBefore(old)
old.remove()
return replace

239
player/videojs.coffee Normal file
View file

@ -0,0 +1,239 @@
sortSources = (sources) ->
if not sources
console.error('sortSources() called with null source list')
return []
qualities = ['2160', '1440', '1080', '720', '540', '480', '360', '240']
pref = String(USEROPTS.default_quality)
if USEROPTS.default_quality == 'best'
pref = '2160'
idx = qualities.indexOf(pref)
if idx < 0
idx = 5 # 480p
qualityOrder = qualities.slice(idx).concat(qualities.slice(0, idx).reverse())
qualityOrder.unshift('auto')
sourceOrder = []
flvOrder = []
for quality in qualityOrder
if quality of sources
flv = []
nonflv = []
sources[quality].forEach((source) ->
source.quality = quality
if source.contentType == 'video/flv'
flv.push(source)
else
nonflv.push(source)
)
sourceOrder = sourceOrder.concat(nonflv)
flvOrder = flvOrder.concat(flv)
return sourceOrder.concat(flvOrder).map((source) ->
type: source.contentType
src: source.link
res: source.quality
label: getSourceLabel(source)
)
getSourceLabel = (source) ->
if source.res is 'auto'
return 'auto'
else
return "#{source.quality}p #{source.contentType.split('/')[1]}"
hasAnyTextTracks = (data) ->
ntracks = data?.meta?.textTracks?.length ? 0
return ntracks > 0
hasAnyAudioTracks = (data) ->
ntracks = data?.meta?.audioTracks?.length ? 0
return ntracks > 0
window.VideoJSPlayer = class VideoJSPlayer extends Player
constructor: (data) ->
if not (this instanceof VideoJSPlayer)
return new VideoJSPlayer(data)
@load(data)
loadPlayer: (data) ->
waitUntilDefined(window, 'videojs', =>
attrs =
width: '100%'
height: '100%'
if @mediaType == 'cm' and hasAnyTextTracks(data)
attrs.crossorigin = 'anonymous'
video = $('<video/>')
.addClass('video-js vjs-default-skin embed-responsive-item')
.attr(attrs)
removeOld(video)
@sources = sortSources(data.meta.direct)
if @sources.length == 0
console.error('VideoJSPlayer::constructor(): data.meta.direct
has no sources!')
@mediaType = null
return
@sourceIdx = 0
# TODO: Refactor VideoJSPlayer to use a preLoad()/load()/postLoad() pattern
# VideoJSPlayer should provide the core functionality and logic for specific
# dependent player types (gdrive) should be an extension
if data.meta.gdrive_subtitles
data.meta.gdrive_subtitles.available.forEach((subt) ->
label = subt.lang_original
if subt.name
label += " (#{subt.name})"
$('<track/>').attr(
src: "/gdvtt/#{data.id}/#{subt.lang}/#{subt.name}.vtt?\
vid=#{data.meta.gdrive_subtitles.vid}"
kind: 'subtitles'
srclang: subt.lang
label: label
).appendTo(video)
)
if data.meta.textTracks
data.meta.textTracks.forEach((track) ->
label = track.name
attrs =
src: track.url
kind: 'subtitles'
type: track.type
label: label
if track.default? and track.default
attrs.default = ''
$('<track/>').attr(attrs).appendTo(video)
)
pluginData =
videoJsResolutionSwitcher:
default: @sources[0].res
if hasAnyAudioTracks(data)
pluginData.audioSwitch =
audioTracks: data.meta.audioTracks,
volume: VOLUME
@player = videojs(video[0],
# https://github.com/Dash-Industry-Forum/dash.js/issues/2184
autoplay: @sources[0].type != 'application/dash+xml',
controls: true,
plugins: pluginData
)
@player.ready(=>
# Have to use updateSrc instead of <source> tags
# see: https://github.com/videojs/video.js/issues/3428
@player.poster(data.meta.thumbnail)
@player.updateSrc(@sources)
@player.on('error', =>
err = @player.error()
if err and err.code == 4
console.error('Caught error, trying next source')
# Does this really need to be done manually?
@sourceIdx++
if @sourceIdx < @sources.length
@player.src(@sources[@sourceIdx])
else
console.error('Out of sources, video will not play')
if @mediaType is 'gd'
if not window.hasDriveUserscript
window.promptToInstallDriveUserscript()
else
window.tellUserNotToContactMeAboutThingsThatAreNotSupported()
)
@setVolume(VOLUME)
@player.on('ended', ->
if CLIENT.leader
socket.emit('playNext')
)
@player.on('pause', =>
@paused = true
if CLIENT.leader
sendVideoUpdate()
)
@player.on('play', =>
@paused = false
if CLIENT.leader
sendVideoUpdate()
)
# Workaround for IE-- even after seeking completes, the loading
# spinner remains.
@player.on('seeked', =>
$('.vjs-waiting').removeClass('vjs-waiting')
)
# Workaround for Chrome-- it seems that the click bindings for
# the subtitle menu aren't quite set up until after the ready
# event finishes, so set a timeout for 1ms to force this code
# not to run until the ready() function returns.
setTimeout(->
$('#ytapiplayer .vjs-subtitles-button .vjs-menu-item').each((i, elem) ->
textNode = elem.childNodes[0]
if textNode.textContent == localStorage.lastSubtitle
elem.click()
elem.onclick = ->
if elem.attributes['aria-checked'].value == 'true'
localStorage.lastSubtitle = textNode.textContent
)
, 1)
)
)
load: (data) ->
@setMediaProperties(data)
# Note: VideoJS does have facilities for loading new videos into the
# existing player object, however it appears to be pretty glitchy when
# a video can't be played (either previous or next video). It's safer
# to just reset the entire thing.
@destroy()
@loadPlayer(data)
play: ->
@paused = false
if @player and @player.readyState() > 0
@player.play()
pause: ->
@paused = true
if @player and @player.readyState() > 0
@player.pause()
seekTo: (time) ->
if @player and @player.readyState() > 0
@player.currentTime(time)
setVolume: (volume) ->
if @player
@player.volume(volume)
getTime: (cb) ->
if @player and @player.readyState() > 0
cb(@player.currentTime())
else
cb(0)
getVolume: (cb) ->
if @player and @player.readyState() > 0
if @player.muted()
cb(0)
else
cb(@player.volume())
else
cb(VOLUME)
destroy: ->
removeOld()
if @player
@player.dispose()

86
player/vimeo.coffee Normal file
View file

@ -0,0 +1,86 @@
window.VimeoPlayer = class VimeoPlayer extends Player
constructor: (data) ->
if not (this instanceof VimeoPlayer)
return new VimeoPlayer(data)
@load(data)
load: (data) ->
@setMediaProperties(data)
waitUntilDefined(window, 'Vimeo', =>
video = $('<iframe/>')
removeOld(video)
video.attr(
src: "https://player.vimeo.com/video/#{data.id}"
allow: 'autoplay; fullscreen'
)
@vimeo = new Vimeo.Player(video[0])
@vimeo.on('ended', =>
if CLIENT.leader
socket.emit('playNext')
)
@vimeo.on('pause', =>
@paused = true
if CLIENT.leader
sendVideoUpdate()
)
@vimeo.on('play', =>
@paused = false
if CLIENT.leader
sendVideoUpdate()
)
@play()
@setVolume(VOLUME)
)
play: ->
@paused = false
if @vimeo
@vimeo.play().catch((error) ->
console.error('vimeo::play():', error)
)
pause: ->
@paused = true
if @vimeo
@vimeo.pause().catch((error) ->
console.error('vimeo::pause():', error)
)
seekTo: (time) ->
if @vimeo
@vimeo.setCurrentTime(time).catch((error) ->
console.error('vimeo::setCurrentTime():', error)
)
setVolume: (volume) ->
if @vimeo
@vimeo.setVolume(volume).catch((error) ->
console.error('vimeo::setVolume():', error)
)
getTime: (cb) ->
if @vimeo
@vimeo.getCurrentTime().then((time) ->
cb(parseFloat(time))
).catch((error) ->
console.error('vimeo::getCurrentTime():', error)
)
else
cb(0)
getVolume: (cb) ->
if @vimeo
@vimeo.getVolume().then((volume) ->
cb(parseFloat(volume))
).catch((error) ->
console.error('vimeo::getVolume():', error)
)
else
cb(VOLUME)

87
player/whepplayer.coffee Normal file
View file

@ -0,0 +1,87 @@
window.WhepPlayer = class WhepPlayer extends Player
constructor: (data) ->
return new WhepPlayer(data) unless this instanceof WhepPlayer
super(data)
@load(data)
load: (data) ->
@ready = false
@paused = true
@setMediaProperties(data)
unless data.meta?.whepURL
u = new URL(data.id)
streemId = u.pathname.split('/').filter(Boolean)[0]
data.meta ?= {}
# Adjust these to whatever your backend expects
data.meta.whepURL = "#{u.origin}/api/whep"
data.meta.streamKey = streemId
{ whepURL, streamKey } = data.meta
# Create video element
videoEl = document.createElement 'video'
videoEl.autoplay = true
videoEl.muted = true
videoEl.controls = true
# Use the standard container swapper
holder = removeOld() # jQuery object
holder.empty().append(videoEl)
@videoEl = videoEl
# ---- WebRTC setup ----
pc = new RTCPeerConnection()
pc.addTransceiver 'audio', direction: 'recvonly'
pc.addTransceiver 'video', direction: 'recvonly'
pc.ontrack = (e) => @videoEl.srcObject = e.streams[0]
pc.onconnectionstatechange = =>
if pc.connectionState is 'connected'
@ready = true
@fire? 'ready'
pc.createOffer()
.then (offer) =>
pc.setLocalDescription offer
fetch whepURL,
method: 'POST'
headers:
Authorization: "Bearer #{streamKey}"
'Content-Type': 'application/sdp'
body: offer.sdp
.then (r) -> r.text()
.then (answer) ->
pc.setRemoteDescription type: 'answer', sdp: answer
.catch (err) ->
console.error 'WHEP negotiation failed:', err
@pc = pc
play: ->
@paused = false
@videoEl?.play?()
pause: ->
@paused = true
@videoEl?.pause?()
seekTo: (t) ->
@videoEl?.currentTime = t if @ready
setVolume: (v) ->
@videoEl?.volume = v if @ready
getTime: (cb) ->
if @ready and @videoEl?
cb @videoEl.currentTime
else
cb 0
getVolume: (cb) ->
if @ready and @videoEl?
cb @videoEl.volume
else
cb VOLUME

96
player/youtube.coffee Normal file
View file

@ -0,0 +1,96 @@
window.YouTubePlayer = class YouTubePlayer extends Player
constructor: (data) ->
if not (this instanceof YouTubePlayer)
return new YouTubePlayer(data)
@setMediaProperties(data)
@pauseSeekRaceCondition = false
waitUntilDefined(window, 'YT', =>
# Even after window.YT is defined, YT.Player may not be, which causes a
# 'YT.Player is not a constructor' error occasionally
waitUntilDefined(YT, 'Player', =>
removeOld()
@yt = new YT.Player('ytapiplayer',
videoId: data.id
playerVars:
autohide: 1
autoplay: 1
controls: 1
iv_load_policy: 3 # iv_load_policy 3 indicates no annotations
rel: 0
events:
onReady: @onReady.bind(this)
onStateChange: @onStateChange.bind(this)
)
)
)
load: (data) ->
@setMediaProperties(data)
if @yt and @yt.ready
@yt.loadVideoById(data.id, data.currentTime)
else
console.error('WTF? YouTubePlayer::load() called but yt is not ready')
onReady: ->
@yt.ready = true
@setVolume(VOLUME)
onStateChange: (ev) ->
# If you pause the video before the first PLAYING
# event is emitted, weird things happen (or at least that was true
# whenever this comment was authored in 2015).
if ev.data == YT.PlayerState.PLAYING and @pauseSeekRaceCondition
@pause()
@pauseSeekRaceCondition = false
if (ev.data == YT.PlayerState.PAUSED and not @paused) or
(ev.data == YT.PlayerState.PLAYING and @paused)
@paused = (ev.data == YT.PlayerState.PAUSED)
if CLIENT.leader
sendVideoUpdate()
if ev.data == YT.PlayerState.ENDED and CLIENT.leader
socket.emit('playNext')
play: ->
@paused = false
if @yt and @yt.ready
@yt.playVideo()
pause: ->
@paused = true
if @yt and @yt.ready
@yt.pauseVideo()
seekTo: (time) ->
if @yt and @yt.ready
@yt.seekTo(time, true)
setVolume: (volume) ->
if @yt and @yt.ready
if volume > 0
# If the player is muted, even if the volume is set,
# the player remains muted
@yt.unMute()
@yt.setVolume(volume * 100)
setQuality: (quality) ->
# https://github.com/calzoneman/sync/issues/726
getTime: (cb) ->
if @yt and @yt.ready
cb(@yt.getCurrentTime())
else
cb(0)
getVolume: (cb) ->
if @yt and @yt.ready
if @yt.isMuted()
cb(0)
else
cb(@yt.getVolume() / 100)
else
cb(VOLUME)

View file

@ -1,520 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
ULList = require("./ullist").ULList;
var Media = require("./media").Media;
var AllPlaylists = {};
function PlaylistItem(media, uid) {
this.media = media;
this.uid = uid;
this.temp = false;
this.queueby = "";
this.prev = null;
this.next = null;
}
PlaylistItem.prototype.pack = function() {
return {
media: this.media.pack(),
uid: this.uid,
temp: this.temp,
queueby: this.queueby
};
}
function Playlist(chan) {
var name = chan.canonical_name;
if(name in AllPlaylists && AllPlaylists[name]) {
var pl = AllPlaylists[name];
if(!pl.dead)
pl.die();
}
this.items = new ULList();
this.current = null;
this.next_uid = 0;
this._leadInterval = false;
this._lastUpdate = 0;
this._counter = 0;
this.leading = true;
this.callbacks = {
"changeMedia": [],
"mediaUpdate": [],
"remove": [],
};
this.lock = false;
this.action_queue = [];
this._qaInterval = false;
AllPlaylists[name] = this;
this.channel = chan;
this.server = chan.server;
var pl = this;
this.on("mediaUpdate", function(m) {
chan.sendAll("mediaUpdate", m.timeupdate());
});
this.on("changeMedia", function(m) {
chan.onVideoChange();
chan.sendAll("setCurrent", pl.current.uid);
chan.sendAll("changeMedia", m.fullupdate());
});
this.on("remove", function(item) {
chan.broadcastPlaylistMeta();
chan.sendAll("delete", {
uid: item.uid
});
});
}
Playlist.prototype.queueAction = function(data) {
this.action_queue.push(data);
if(this._qaInterval)
return;
var pl = this;
this._qaInterval = setInterval(function() {
var data = pl.action_queue.shift();
if(data.waiting) {
if(!("expire" in data))
data.expire = Date.now() + 10000;
if(Date.now() < data.expire)
pl.action_queue.unshift(data);
}
else
data.fn();
if(pl.action_queue.length == 0) {
clearInterval(pl._qaInterval);
pl._qaInterval = false;
}
}, 100);
}
Playlist.prototype.dump = function() {
var arr = this.items.toArray();
var pos = 0;
for(var i in arr) {
if(this.current && arr[i].uid == this.current.uid) {
pos = i;
break;
}
}
var time = 0;
if(this.current)
time = this.current.media.currentTime;
return {
pl: arr,
pos: pos,
time: time
};
}
Playlist.prototype.die = function () {
this.clear();
if(this._leadInterval) {
clearInterval(this._leadInterval);
this._leadInterval = false;
}
if(this._qaInterval) {
clearInterval(this._qaInterval);
this._qaInterval = false;
}
//for(var key in this)
// delete this[key];
this.dead = true;
}
Playlist.prototype.load = function(data, callback) {
this.clear();
for(var i in data.pl) {
var e = data.pl[i].media;
var m = new Media(e.id, e.title, e.seconds, e.type);
var it = this.makeItem(m);
it.temp = data.pl[i].temp;
it.queueby = data.pl[i].queueby;
this.items.append(it);
if(i == parseInt(data.pos)) {
this.current = it;
}
}
if(callback)
callback();
}
Playlist.prototype.on = function(ev, fn) {
if(typeof fn === "undefined") {
var pl = this;
return function() {
for(var i = 0; i < pl.callbacks[ev].length; i++) {
pl.callbacks[ev][i].apply(this, arguments);
}
}
}
else if(typeof fn === "function") {
this.callbacks[ev].push(fn);
}
}
Playlist.prototype.makeItem = function(media) {
return new PlaylistItem(media, this.next_uid++);
}
Playlist.prototype.add = function(item, pos) {
var success;
if(pos == "append")
success = this.items.append(item);
else if(pos == "prepend")
success = this.items.prepend(item);
else
success = this.items.insertAfter(item, pos);
if(success && this.items.length == 1) {
this.current = item;
this.startPlayback();
}
return success;
}
Playlist.prototype.addCachedMedia = function(data, callback) {
var pos = "append";
if(data.pos == "next") {
if(!this.current)
pos = "prepend";
else
pos = this.current.uid;
}
var it = this.makeItem(data.media);
it.temp = data.temp;
it.queueby = data.queueby;
var pl = this;
var action = {
fn: function() {
if(pl.add(it, pos))
callback(false, it);
},
waiting: false
};
this.queueAction(action);
}
Playlist.prototype.addMedia = function(data, callback) {
if(data.type == "yp") {
this.addYouTubePlaylist(data, callback);
return;
}
var pos = "append";
if(data.pos == "next") {
if(!this.current)
pos = "prepend";
else
pos = this.current.uid;
}
var it = this.makeItem(null);
var pl = this;
var action = {
fn: function() {
if(pl.add(it, pos)) {
callback(false, it);
}
},
waiting: true
};
this.queueAction(action);
// Pre-cached data
if(typeof data.title === "string" &&
typeof data.seconds === "number") {
if(data.maxlength && data.seconds > data.maxlength) {
action.expire = 0;
callback("Media is too long!", null);
return;
}
it.media = new Media(data.id, data.title, data.seconds, data.type);
action.waiting = false;
return;
}
this.server.infogetter.getMedia(data.id, data.type, function(err, media) {
if(err) {
action.expire = 0;
callback(err, null);
return;
}
if(data.maxlength && media.seconds > data.maxlength) {
action.expire = 0;
callback("Media is too long!", null);
return;
}
it.media = media;
it.temp = data.temp;
it.queueby = data.queueby;
action.waiting = false;
});
}
Playlist.prototype.addMediaList = function(data, callback) {
var start = false;
if(data.pos == "next") {
data.list = data.list.reverse();
start = data.list[data.list.length - 1];
}
if(this.items.length != 0)
start = false;
var pl = this;
for(var i = 0; i < data.list.length; i++) {
var x = data.list[i];
x.pos = data.pos;
if(start && x == start) {
pl.addMedia(x, function (err, item) {
if(err) {
callback(err, item);
}
else {
callback(err, item);
pl.current = item;
pl.startPlayback();
}
});
}
else {
pl.addMedia(x, callback);
}
}
}
Playlist.prototype.addYouTubePlaylist = function(data, callback) {
var pos = "append";
if(data.pos == "next") {
if(!this.current)
pos = "prepend";
else
pos = this.current.uid;
}
var pl = this;
this.server.infogetter.getMedia(data.id, data.type, function(err, vids) {
if(err) {
callback(err, null);
return;
}
if(data.pos === "next")
vids.reverse();
vids.forEach(function(media) {
if(data.maxlength && media.seconds > data.maxlength) {
callback("Media is too long!", null);
return;
}
var it = pl.makeItem(media);
it.temp = data.temp;
it.queueby = data.queueby;
pl.queueAction({
fn: function() {
if(pl.add(it, pos))
callback(false, it);
},
});
});
});
}
Playlist.prototype.remove = function(uid, callback) {
var pl = this;
this.queueAction({
fn: function() {
var item = pl.items.find(uid);
if(pl.items.remove(uid)) {
if(callback)
callback();
if(item == pl.current)
pl._next();
}
},
waiting: false
});
}
Playlist.prototype.move = function(from, after, callback) {
var pl = this;
this.queueAction({
fn: function() {
pl._move(from, after, callback);
},
waiting: false
});
}
Playlist.prototype._move = function(from, after, callback) {
var it = this.items.find(from);
if(!this.items.remove(from))
return;
if(after === "prepend") {
if(!this.items.prepend(it))
return;
}
else if(after === "append") {
if(!this.items.append(it))
return;
}
else if(!this.items.insertAfter(it, after))
return;
callback();
}
Playlist.prototype.next = function() {
if(!this.current)
return;
var it = this.current;
if(it.temp) {
var pl = this;
this.remove(it.uid, function() {
pl.on("remove")(it);
});
}
else {
this._next();
}
return this.current;
}
Playlist.prototype._next = function() {
if(!this.current)
return;
this.current = this.current.next;
if(this.current === null && this.items.first !== null)
this.current = this.items.first;
if(this.current) {
this.startPlayback();
}
}
Playlist.prototype.jump = function(uid) {
if(!this.current)
return false;
var jmp = this.items.find(uid);
if(!jmp)
return false;
var it = this.current;
this.current = jmp;
if(this.current) {
this.startPlayback();
}
if(it.temp) {
var pl = this;
this.remove(it.uid, function () {
pl.on("remove")(it);
});
}
return this.current;
}
Playlist.prototype.clear = function() {
this.items.clear();
this.next_uid = 0;
this.current = null;
clearInterval(this._leadInterval);
}
Playlist.prototype.lead = function(lead) {
this.leading = lead;
var pl = this;
if(!this.leading && this._leadInterval) {
clearInterval(this._leadInterval);
this._leadInterval = false;
}
else if(this.leading && !this._leadInterval) {
this._leadInterval = setInterval(function() {
pl._leadLoop();
}, 1000);
}
}
Playlist.prototype.startPlayback = function(time) {
if(!this.current || !this.current.media)
return false;
this.current.media.paused = false;
this.current.media.currentTime = time || -1;
var pl = this;
if(this._leadInterval) {
clearInterval(this._leadInterval);
this._leadInterval = false;
}
this.on("changeMedia")(this.current.media);
if(this.leading && !isLive(this.current.media.type)) {
this._lastUpdate = Date.now();
this._leadInterval = setInterval(function() {
pl._leadLoop();
}, 1000);
}
}
function isLive(type) {
return type == "li" // Livestream.com
|| type == "tw" // Twitch.tv
|| type == "jt" // Justin.tv
|| type == "rt" // RTMP
|| type == "jw" // JWPlayer
|| type == "us" // Ustream.tv
|| type == "im" // Imgur album
|| type == "cu";// Custom embed
}
const UPDATE_INTERVAL = 5;
Playlist.prototype._leadLoop = function() {
if(this.current == null)
return;
if(this.channel.name == "") {
this.die();
return;
}
this.current.media.currentTime += (Date.now() - this._lastUpdate) / 1000.0;
this._lastUpdate = Date.now();
this._counter++;
if(this.current.media.currentTime >= this.current.media.seconds + 2) {
this.next();
}
else if(this._counter % UPDATE_INTERVAL == 0) {
this.on("mediaUpdate")(this.current.media);
}
}
module.exports = Playlist;

46
poll.js
View file

@ -1,46 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var Poll = function(initiator, title, options) {
this.initiator = initiator;
this.title = title;
this.options = options;
this.counts = new Array(options.length);
for(var i = 0; i < this.counts.length; i++) {
this.counts[i] = 0;
}
this.votes = {};
}
Poll.prototype.vote = function(ip, option) {
if(!(ip in this.votes) || this.votes[ip] == null) {
this.votes[ip] = option;
this.counts[option]++;
}
}
Poll.prototype.unvote = function(ip) {
if(ip in this.votes && this.votes[ip] != null) {
this.counts[this.votes[ip]]--;
this.votes[ip] = null;
}
}
Poll.prototype.packUpdate = function() {
return {
title: this.title,
options: this.options,
counts: this.counts,
initiator: this.initiator
}
}
exports.Poll = Poll;

14
postinstall.sh Executable file
View file

@ -0,0 +1,14 @@
#!/bin/sh
set -e
if ! command -v npm >/dev/null; then
echo "Could not find npm in \$PATH"
exit 1
fi
echo "Building from src/ to lib/"
npm run build-server
echo "Building from player/ to www/js/player.js"
npm run build-player
echo "Done"

55
rank.js
View file

@ -1,55 +0,0 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Calvin Montgomery
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
exports.Anonymous = -1;
exports.Guest = 0;
exports.Member = 1;
exports.Moderator = 2;
exports.Owner = 3;
exports.Siteadmin = 255;
var permissions = {
acp : exports.Siteadmin,
announce : exports.Siteadmin,
registerChannel : exports.Owner,
acl : exports.Owner,
chatFilter : exports.Owner,
setcss : exports.Owner,
setjs : exports.Owner,
channelperms : exports.Owner,
queue : exports.Moderator,
assignLeader : exports.Moderator,
kick : exports.Moderator,
ipban : exports.Moderator,
ban : exports.Moderator,
promote : exports.Moderator,
qlock : exports.Moderator,
poll : exports.Moderator,
shout : exports.Moderator,
channelOpts : exports.Moderator,
jump : exports.Moderator,
updateMotd : exports.Moderator,
drink : exports.Moderator,
seeVoteskip : exports.Moderator,
uncache : exports.Moderator,
seenlogins : exports.Moderator,
settemp : exports.Moderator,
search : exports.Guest,
chat : exports.Guest,
};
// Check if someone has permission to do shit
exports.hasPermission = function(user, what) {
if(what in permissions) {
return user.rank >= permissions[what];
}
else return false;
}

2
run.sh
View file

@ -2,7 +2,7 @@
while :
do
node server.js
node index.js
sleep 2
done

122
servcmd.sh.js Executable file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env node
/*
** CyTube Service Socket Commandline
*/
const readline = require('readline');
const spawn = require('child_process').spawn;
const util = require('util');
const net = require('net');
const fs = require('fs');
const COMPLETIONS = [
"/delete_old_tables",
"/gc",
"/globalban",
"/reload",
"/reloadcert",
"/reload-partitions",
"/switch",
"/unglobalban",
"/unloadchan"
];
var Config = require("./lib/config");
Config.load("config.yaml");
if(!Config.get("service-socket.enabled")){
console.error('The Service Socket is not enabled.');
process.exit(1);
}
const SOCKETFILE = Config.get("service-socket.socket");
// Wipe the TTY
process.stdout.write('\x1Bc');
var commandline, eventlog, syslog, errorlog;
var client = net.createConnection(SOCKETFILE).on('connect', () => {
commandline = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: tabcomplete
});
commandline.setPrompt("> ", 2);
commandline.on("line", function(line) {
if(line === 'exit'){ return cleanup(); }
if(line === 'quit'){ return cleanup(); }
if(line.match(/^\/globalban/) && line.split(/\s+/).length === 2){
console.log('You must provide a reason')
return commandline.prompt();
}
client.write(line);
commandline.prompt();
});
commandline.on('close', function() {
return cleanup();
});
commandline.on("SIGINT", function() {
commandline.clearLine();
commandline.question("Terminate connection? ", function(answer) {
return answer.match(/^y(es)?$/i) ? cleanup() : commandline.output.write("> ");
});
});
commandline.prompt();
console.log = function() { cmdouthndlr("log", arguments); }
console.warn = function() { cmdouthndlr("warn", arguments); }
console.error = function() { cmdouthndlr("error", arguments); }
// console.info is reserved in this script for the exit message
// this prevents an extraneous final prompt from readline on terminate
eventlog = spawn('tail', ['-f', 'events.log']);
eventlog.stdout.on('data', function (data) {
console.log(data.toString().replace(/^(.+)$/mg, 'events: $1'));
});
syslog = spawn('tail', ['-f', 'sys.log']);
syslog.stdout.on('data', function (data) {
console.log(data.toString().replace(/^(.+)$/mg, 'sys: $1'));
});
errorlog = spawn('tail', ['-f', 'error.log']);
errorlog.stdout.on('data', function (data) {
console.log(data.toString().replace(/^(.+)$/mg, 'error: $1'));
});
}).on('data', (msg) => {
msg = msg.toString();
if(msg === '__disconnect'){
console.log('Server shutting down.');
return cleanup();
}
// Generic message handler
console.log('server: ', data)
}).on('error', (data) => {
console.error('Unable to connect to Service Socket.', data);
process.exit(1);
});
function cmdouthndlr(type, args) {
var t = Math.ceil((commandline.line.length + 3) / process.stdout.columns);
var text = util.format.apply(console, args);
commandline.output.write("\n\x1B[" + t + "A\x1B[0J");
commandline.output.write(text + "\n");
commandline.output.write(Array(t).join("\n\x1B[E"));
commandline._refreshLine();
}
function cleanup(){
console.info('\n',"Terminating.",'\n');
eventlog.kill('SIGTERM');
syslog.kill('SIGTERM');
client.end();
process.exit(0);
}
function tabcomplete(line) {
return [COMPLETIONS.filter((cv)=>{ return cv.indexOf(line) == 0; }), line];
}

248
server.js
View file

@ -1,248 +0,0 @@
var path = require("path");
var fs = require("fs");
var express = require("express");
var Config = require("./config");
var Logger = require("./logger");
var Channel = require("./channel");
var User = require("./user");
const VERSION = "2.3.1";
function getIP(req) {
var raw = req.connection.remoteAddress;
var forward = req.header("x-forwarded-for");
if(Server.cfg["trust-x-forward"] && forward) {
var ip = forward.split(",")[0];
Logger.syslog.log("REVPROXY " + raw + " => " + ip);
return ip;
}
return raw;
}
function getSocketIP(socket) {
var raw = socket.handshake.address.address;
if(Server.cfg["trust-x-forward"]) {
if(typeof socket.handshake.headers["x-forwarded-for"] == "string") {
var ip = socket.handshake.headers["x-forwarded-for"]
.split(",")[0];
Logger.syslog.log("REVPROXY " + raw + " => " + ip);
return ip;
}
}
return raw;
}
var Server = {
channels: [],
channelLoaded: function (name) {
for(var i in this.channels) {
if(this.channels[i].canonical_name == name.toLowerCase())
return true;
}
return false;
},
getChannel: function (name) {
for(var i in this.channels) {
if(this.channels[i].canonical_name == name.toLowerCase())
return this.channels[i];
}
var c = new Channel(name, this);
this.channels.push(c);
return c;
},
unloadChannel: function(chan) {
if(chan.registered)
chan.saveDump();
chan.playlist.die();
chan.logger.close();
for(var i in this.channels) {
if(this.channels[i].canonical_name == chan.canonical_name) {
this.channels.splice(i, 1);
break;
}
}
chan.name = "";
},
stats: null,
app: null,
io: null,
httpserv: null,
ioserv: null,
db: null,
ips: {},
acp: null,
httpaccess: null,
logHTTP: function (req, status) {
if(status === undefined)
status = 200;
var ip = req.connection.remoteAddress;
var ip2 = false;
if(this.cfg["trust-x-forward"])
ip2 = req.header("x-forwarded-for") || req.header("cf-connecting-ip");
var ipstr = !ip2 ? ip : ip + " (X-Forwarded-For " + ip2 + ")";
var url = req.url;
// Remove query
if(url.indexOf("?") != -1)
url = url.substring(0, url.lastIndexOf("?"));
this.httpaccess.log([ipstr, req.method, url, status, req.headers["user-agent"]].join(" "));
},
init: function () {
this.httpaccess = new Logger.Logger("httpaccess.log");
this.app = express();
// channel path
this.app.get("/r/:channel(*)", function (req, res, next) {
var c = req.params.channel;
if(!c.match(/^[\w-_]+$/)) {
res.redirect("/" + c);
}
else {
this.logHTTP(req);
res.sendfile(__dirname + "/www/channel.html");
}
}.bind(this));
// api path
this.api = require("./api")(this);
this.app.get("/api/:apireq(*)", function (req, res, next) {
this.logHTTP(req);
this.api.handle(req.url.substring(5), req, res);
}.bind(this));
this.app.get("/", function (req, res, next) {
this.logHTTP(req);
res.sendfile(__dirname + "/www/index.html");
}.bind(this));
// default path
this.app.get("/:thing(*)", function (req, res, next) {
var opts = {
root: __dirname + "/www",
maxAge: this.cfg["asset-cache-ttl"]
}
res.sendfile(req.params.thing, opts, function (err) {
if(err) {
this.logHTTP(req, err.status);
// Damn path traversal attacks
if(req.params.thing.indexOf("%2e") != -1) {
res.send("Don't try that again, I'll ban you");
Logger.syslog.log("WARNING: Attempted path "+
"traversal from /" + getIP(req));
Logger.syslog.log("URL: " + req.url);
}
// Something actually went wrong
else {
// Status codes over 500 are server errors
if(err.status >= 500)
Logger.errlog.log(err);
res.send(err.status);
}
}
else {
this.logHTTP(req);
}
}.bind(this));
}.bind(this));
// fallback
this.app.use(function (err, req, res, next) {
this.logHTTP(req, err.status);
if(err.status == 404) {
res.send(404);
} else {
next(err);
}
}.bind(this));
// bind servers
this.httpserv = this.app.listen(Server.cfg["web-port"],
Server.cfg["express-host"]);
this.ioserv = express().listen(Server.cfg["io-port"],
Server.cfg["express-host"]);
// init socket.io
this.io = require("socket.io").listen(this.ioserv);
this.io.set("log level", 1);
this.io.sockets.on("connection", function (socket) {
var ip = getSocketIP(socket);
socket._ip = ip;
if(this.db.checkGlobalBan(ip)) {
Logger.syslog.log("Disconnecting " + ip + " - gbanned");
socket.emit("kick", {
reason: "You're globally banned."
});
socket.disconnect(true);
return;
}
socket.on("disconnect", function () {
this.ips[ip]--;
}.bind(this));
if(!(ip in this.ips))
this.ips[ip] = 0;
this.ips[ip]++;
if(this.ips[ip] > Server.cfg["ip-connection-limit"]) {
socket.emit("kick", {
reason: "Too many connections from your IP address"
});
socket.disconnect(true);
return;
}
// finally a valid user
Logger.syslog.log("Accepted socket from /" + socket._ip);
new User(socket, this);
}.bind(this));
// init database
this.db = require("./database");
this.db.setup(Server.cfg);
this.db.init();
// init ACP
this.acp = require("./acp")(this);
// init stats
this.stats = require("./stats")(this);
// init media retriever
this.infogetter = require("./get-info")(this);
},
shutdown: function () {
Logger.syslog.log("Unloading channels");
for(var i in this.channels) {
if(this.channels[i].registered) {
Logger.syslog.log("Saving /r/" + this.channels[i].name);
this.channels[i].saveDump();
}
}
Logger.syslog.log("Goodbye");
process.exit(0);
}
};
Logger.syslog.log("Starting CyTube v" + VERSION);
fs.exists("chanlogs", function (exists) {
exists || fs.mkdir("chanlogs");
});
fs.exists("chandump", function (exists) {
exists || fs.mkdir("chandump");
});
Config.load(Server, "cfg.json", function () {
Server.init();
if(!Server.cfg["debug"]) {
process.on("uncaughtException", function (err) {
Logger.errlog.log("[SEVERE] Uncaught Exception: " + err);
Logger.errlog.log(err.stack);
});
process.on("SIGINT", function () {
Server.shutdown();
});
}
});

1
src/.eslintrc.json Normal file
View file

@ -0,0 +1 @@
{ "env": { "node": true } }

59
src/account.js Normal file
View file

@ -0,0 +1,59 @@
import db from './database';
import Promise from 'bluebird';
const dbGetGlobalRank = Promise.promisify(db.users.getGlobalRank);
const dbMultiGetGlobalRank = Promise.promisify(db.users.getGlobalRanks);
const dbGetChannelRank = Promise.promisify(db.channels.getRank);
const dbMultiGetChannelRank = Promise.promisify(db.channels.getRanks);
const dbGetAliases = Promise.promisify(db.getAliases);
const DEFAULT_PROFILE = Object.freeze({ image: '', text: '' });
class Account {
constructor(ip, user, aliases) {
this.ip = ip;
this.user = user;
this.aliases = aliases;
this.channelRank = -1;
this.guestName = null;
this.update();
}
update() {
if (this.user !== null) {
this.name = this.user.name;
this.globalRank = this.user.global_rank;
} else if (this.guestName !== null) {
this.name = this.guestName;
this.globalRank = 0;
} else {
this.name = '';
this.globalRank = -1;
}
this.lowername = this.name.toLowerCase();
this.effectiveRank = Math.max(this.channelRank, this.globalRank);
this.profile = (this.user === null) ? DEFAULT_PROFILE : this.user.profile;
}
}
module.exports.Account = Account;
module.exports.rankForName = async function rankForNameAsync(name, channel) {
const [globalRank, channelRank] = await Promise.all([
dbGetGlobalRank(name),
dbGetChannelRank(channel, name)
]);
return Math.max(globalRank, channelRank);
};
module.exports.rankForIP = async function rankForIP(ip, channel) {
const aliases = await dbGetAliases(ip);
const [globalRanks, channelRanks] = await Promise.all([
dbMultiGetGlobalRank(aliases),
dbMultiGetChannelRank(channel, aliases)
]);
return Math.max.apply(Math, globalRanks.concat(channelRanks));
};

285
src/acp.js Normal file
View file

@ -0,0 +1,285 @@
var Logger = require("./logger");
var Server = require("./server");
var db = require("./database");
var util = require("./utilities");
import { v4 as uuidv4 } from 'uuid';
function eventUsername(user) {
return user.getName() + "@" + user.realip;
}
function handleAnnounce(user, data) {
var sv = Server.getServer();
sv.announce({
id: uuidv4(),
title: data.title,
text: data.content,
from: user.getName()
});
Logger.eventlog.log("[acp] " + eventUsername(user) + " opened announcement `" +
data.title + "`");
}
function handleAnnounceClear(user) {
Server.getServer().announce(null);
Logger.eventlog.log("[acp] " + eventUsername(user) + " cleared announcement");
}
function handleGlobalBan(user, data) {
const globalBanDB = db.getGlobalBanDB();
globalBanDB.addGlobalIPBan(data.ip, data.note).then(() => {
Logger.eventlog.log("[acp] " + eventUsername(user) + " global banned " + data.ip);
return globalBanDB.listGlobalBans().then(bans => {
// Why is it called reason in the DB and note in the socket frame?
// Who knows...
const mappedBans = bans.map(ban => {
return { ip: ban.ip, note: ban.reason };
});
user.socket.emit("acp-gbanlist", mappedBans);
});
}).catch(error => {
user.socket.emit("errMessage", {
msg: error.message
});
});
}
function handleGlobalBanDelete(user, data) {
const globalBanDB = db.getGlobalBanDB();
globalBanDB.removeGlobalIPBan(data.ip).then(() => {
Logger.eventlog.log("[acp] " + eventUsername(user) + " un-global banned " +
data.ip);
return globalBanDB.listGlobalBans().then(bans => {
// Why is it called reason in the DB and note in the socket frame?
// Who knows...
const mappedBans = bans.map(ban => {
return { ip: ban.ip, note: ban.reason };
});
user.socket.emit("acp-gbanlist", mappedBans);
});
}).catch(error => {
user.socket.emit("errMessage", {
msg: error.message
});
});
}
function handleListUsers(user, data) {
var value = data.value;
var field = data.field;
value = (typeof value !== 'string') ? '' : value;
field = (typeof field !== 'string') ? 'name' : field;
var fields = ["id", "name", "global_rank", "email", "ip", "time"];
if(!fields.includes(field)){
user.socket.emit("errMessage", {
msg: `The field "${field}" doesn't exist or isn't searchable.`
});
return;
}
db.users.search(field, value, fields, function (err, users) {
if (err) {
user.socket.emit("errMessage", {
msg: err
});
return;
}
user.socket.emit("acp-list-users", users);
});
}
function handleSetRank(user, data) {
var name = data.name;
var rank = data.rank;
if (typeof name !== "string" || typeof rank !== "number") {
return;
}
if (rank >= user.global_rank) {
user.socket.emit("errMessage", {
msg: "You are not permitted to promote others to equal or higher rank than " +
"yourself."
});
return;
}
db.users.getGlobalRank(name, function (err, oldrank) {
if (err) {
user.socket.emit("errMessage", {
msg: err
});
return;
}
if (oldrank >= user.global_rank) {
user.socket.emit("errMessage", {
msg: "You are not permitted to change the rank of users who rank " +
"higher than you."
});
return;
}
db.users.setGlobalRank(name, rank, function (err) {
if (err) {
user.socket.emit("errMessage", {
msg: err
});
} else {
Logger.eventlog.log("[acp] " + eventUsername(user) + " set " + name +
"'s global_rank to " + rank);
user.socket.emit("acp-set-rank", data);
}
});
});
}
function handleResetPassword(user, data, ack) {
var name = data.name;
var email = data.email;
if (typeof name !== "string" || typeof email !== "string") {
return;
}
db.users.getGlobalRank(name, function (err, rank) {
if (rank >= user.global_rank) {
user.socket.emit("errMessage", {
msg: "You don't have permission to reset the password for " + name
});
return;
}
var hash = util.sha1(util.randomSalt(64));
var expire = Date.now() + 86400000;
db.addPasswordReset({
ip: "",
name: name,
email: email,
hash: hash,
expire: expire
}, function (err) {
if (err) {
ack && ack({ error: err });
return;
}
Logger.eventlog.log("[acp] " + eventUsername(user) + " initialized a " +
"password recovery for " + name);
ack && ack({ hash });
});
});
}
function handleListChannels(user, data) {
var field = data.field;
var value = data.value;
if (typeof field !== "string" || typeof value !== "string") {
return;
}
var dbfunc;
if (field === "owner") {
dbfunc = db.channels.searchOwner;
} else {
dbfunc = db.channels.search;
}
dbfunc(value, function (err, rows) {
if (err) {
user.socket.emit("errMessage", {
msg: err
});
return;
}
user.socket.emit("acp-list-channels", rows);
});
}
function handleDeleteChannel(user, data) {
var name = data.name;
if (typeof data.name !== "string") {
return;
}
var sv = Server.getServer();
if (sv.isChannelLoaded(name)) {
sv.getChannel(name).users.forEach(function (u) {
u.kick("Channel shutting down");
});
}
db.channels.drop(name, function (err) {
Logger.eventlog.log("[acp] " + eventUsername(user) + " deleted channel " + name);
if (err) {
user.socket.emit("errMessage", {
msg: err
});
} else {
user.socket.emit("acp-delete-channel", {
name: name
});
}
});
}
function handleListActiveChannels(user) {
user.socket.emit("acp-list-activechannels", Server.getServer().packChannelList(false, true));
}
function handleForceUnload(user, data) {
var name = data.name;
if (typeof name !== "string") {
return;
}
var sv = Server.getServer();
if (!sv.isChannelLoaded(name)) {
return;
}
var chan = sv.getChannel(name);
var users = Array.prototype.slice.call(chan.users);
chan.emit("empty");
users.forEach(function (u) {
u.kick("Channel shutting down");
});
Logger.eventlog.log("[acp] " + eventUsername(user) + " forced unload of " + name);
}
function init(user) {
var s = user.socket;
s.on("acp-announce", handleAnnounce.bind(this, user));
s.on("acp-announce-clear", handleAnnounceClear.bind(this, user));
s.on("acp-gban", handleGlobalBan.bind(this, user));
s.on("acp-gban-delete", handleGlobalBanDelete.bind(this, user));
s.on("acp-list-users", handleListUsers.bind(this, user));
s.on("acp-set-rank", handleSetRank.bind(this, user));
s.on("acp-reset-password", handleResetPassword.bind(this, user));
s.on("acp-list-channels", handleListChannels.bind(this, user));
s.on("acp-delete-channel", handleDeleteChannel.bind(this, user));
s.on("acp-list-activechannels", handleListActiveChannels.bind(this, user));
s.on("acp-force-unload", handleForceUnload.bind(this, user));
const globalBanDB = db.getGlobalBanDB();
globalBanDB.listGlobalBans().then(bans => {
// Why is it called reason in the DB and note in the socket frame?
// Who knows...
const mappedBans = bans.map(ban => {
return { ip: ban.ip, note: ban.reason };
});
user.socket.emit("acp-gbanlist", mappedBans);
}).catch(error => {
user.socket.emit("errMessage", {
msg: error.message
});
});
Logger.eventlog.log("[acp] Initialized ACP for " + eventUsername(user));
}
module.exports.init = init;

54
src/asyncqueue.js Normal file
View file

@ -0,0 +1,54 @@
var AsyncQueue = function () {
this._q = [];
this._lock = false;
this._tm = 0;
};
AsyncQueue.prototype.next = function () {
if (this._q.length > 0) {
if (!this.lock())
return;
var item = this._q.shift();
var fn = item[0];
this._tm = Date.now() + item[1];
fn(this);
}
};
AsyncQueue.prototype.lock = function () {
if (this._lock) {
if (this._tm > 0 && Date.now() > this._tm) {
this._tm = 0;
return true;
}
return false;
}
this._lock = true;
return true;
};
AsyncQueue.prototype.release = function () {
var self = this;
if (!self._lock)
return false;
self._lock = false;
setImmediate(function () {
self.next();
});
return true;
};
AsyncQueue.prototype.queue = function (fn) {
var self = this;
self._q.push([fn, 20000]);
self.next();
};
AsyncQueue.prototype.reset = function () {
this._q = [];
this._lock = false;
};
module.exports = AsyncQueue;

183
src/bgtask.js Normal file
View file

@ -0,0 +1,183 @@
/*
bgtask.js
Registers background jobs to run periodically while the server is
running.
*/
var Config = require("./config");
var db = require("./database");
var Promise = require("bluebird");
const shows = require('./shows');
const calendarDB = require('./database/calendar-integrations');
const integrationsApi = require('./web/routes/api/integrations');
const LOGGER = require('@calzoneman/jsli')('bgtask');
var init = null;
/* Alias cleanup */
function initAliasCleanup() {
var CLEAN_INTERVAL = parseInt(Config.get("aliases.purge-interval"));
var CLEAN_EXPIRE = parseInt(Config.get("aliases.max-age"));
setInterval(function () {
db.cleanOldAliases(CLEAN_EXPIRE, function (err) {
LOGGER.info("Cleaned old aliases");
if (err)
LOGGER.error(err);
});
}, CLEAN_INTERVAL);
}
/* Password reset cleanup */
function initPasswordResetCleanup() {
var CLEAN_INTERVAL = 8*60*60*1000;
setInterval(function () {
db.cleanOldPasswordResets(function (err) {
if (err)
LOGGER.error(err);
});
}, CLEAN_INTERVAL);
}
function initChannelDumper(Server) {
const chanPath = Config.get('channel-path');
var CHANNEL_SAVE_INTERVAL = parseInt(Config.get("channel-save-interval"))
* 60000;
setInterval(function () {
if (Server.channels.length === 0) {
return;
}
var wait = CHANNEL_SAVE_INTERVAL / Server.channels.length;
LOGGER.info(`Saving channels with delay ${wait}`);
Promise.reduce(Server.channels, (_, chan) => {
return Promise.delay(wait).then(async () => {
if (!chan.dead && chan.users && chan.users.length > 0) {
try {
await chan.saveState();
LOGGER.info(`Saved /${chanPath}/${chan.name}`);
} catch (error) {
LOGGER.error(
'Failed to save /%s/%s: %s',
chanPath,
chan ? chan.name : '<undefined>',
error.stack
);
}
}
}).catch(error => {
LOGGER.error(`Failed to save channel: ${error.stack}`);
});
}, 0).catch(error => {
LOGGER.error(`Failed to save channels: ${error.stack}`);
});
}, CHANNEL_SAVE_INTERVAL);
}
function initAccountCleanup() {
setInterval(() => {
(async () => {
let rows = await db.users.findAccountsPendingDeletion();
for (let row of rows) {
try {
await db.users.purgeAccount(row.id);
LOGGER.info('Purged account from request %j', row);
} catch (error) {
LOGGER.error('Error purging account %j: %s', row, error.stack);
}
}
})().catch(error => {
LOGGER.error('Error purging deleted accounts: %s', error.stack);
});
}, 3600 * 1000);
}
function initShowScheduler() {
var SCHEDULE_INTERVAL = 15 * 1000;
var running = false;
setInterval(async () => {
if (running) {
return;
}
running = true;
try {
await shows.pollAndRunDueShows();
} catch (error) {
LOGGER.error('Show scheduler failure: %s', error.stack || error);
} finally {
running = false;
}
}, SCHEDULE_INTERVAL);
}
function initCalendarAutoSyncScheduler() {
const AUTO_SYNC_INTERVAL_MS = 60 * 1000;
const AUTO_SYNC_PERIOD_MINUTES = 30;
let running = false;
function inStaggerSlot(integrationId, now) {
const slot = Number(integrationId || 0) % AUTO_SYNC_PERIOD_MINUTES;
const minute = new Date(now).getUTCMinutes() % AUTO_SYNC_PERIOD_MINUTES;
return slot === minute;
}
setInterval(async () => {
if (running) {
return;
}
running = true;
try {
const now = Date.now();
const rows = await calendarDB.listConnectedByProvider('google');
for (const integration of rows) {
if (!inStaggerSlot(integration.id, now)) {
continue;
}
if (integration.last_sync_at && now - integration.last_sync_at < AUTO_SYNC_PERIOD_MINUTES * 60 * 1000) {
continue;
}
try {
await integrationsApi.syncIntegrationNow({
provider: 'google',
integration,
channelRow: { id: integration.channel_id },
enforceCooldown: false
});
} catch (err) {
LOGGER.warn(
'Auto calendar sync failed integration=%s channel=%s: %s',
integration.id,
integration.channel_id,
err && (err.stack || err.message) || err
);
}
}
} catch (err) {
LOGGER.error('Calendar auto-sync scheduler failure: %s', err.stack || err);
} finally {
running = false;
}
}, AUTO_SYNC_INTERVAL_MS);
}
module.exports = function (Server) {
if (init === Server) {
LOGGER.warn("Attempted to re-init background tasks");
return;
}
init = Server;
initAliasCleanup();
initChannelDumper(Server);
initPasswordResetCleanup();
initAccountCleanup();
initShowScheduler();
initCalendarAutoSyncScheduler();
};

Some files were not shown because too many files have changed in this diff Show more