Compare commits
108 commits
chores
...
postgreSQL
Author | SHA1 | Date | |
---|---|---|---|
db6253cd04 | |||
secretspecter | 4846b22cd1 | ||
secretspecter | 0b806cec92 | ||
227244e2d0 | |||
6f47ed42db | |||
98bfb6736e | |||
2c541448a2 | |||
21d7f16413 | |||
87198bd4e7 | |||
986207b46b | |||
ed410fdebe | |||
1a9d920884 | |||
c78ef333da | |||
fad1da7ab4 | |||
d37e69e1a6 | |||
1e2dcee4fa | |||
6ec2f3d491 | |||
306e3adde8 | |||
99740a3673 | |||
913348d46e | |||
ae5dbf5f48 | |||
8338fe2f25 | |||
7921f41174 | |||
50e2692896 | |||
9e0f7b8efa | |||
fd9586e0da | |||
f185e6c3ea | |||
2cf26cdc4c | |||
008c24f892 | |||
a398e3a6fa | |||
aa04f0d034 | |||
e7f0aa98be | |||
0f9d778a27 | |||
119b6a62b8 | |||
9d00d9666d | |||
f6ba5b71e8 | |||
9b05e2eb8c | |||
911558760f | |||
45217ccad8 | |||
aeb5de85b6 | |||
53911ab9f0 | |||
a2c4ea5036 | |||
517058bef3 | |||
1790d5b569 | |||
97b8d1b4b7 | |||
25ddc336e0 | |||
498272b128 | |||
26f6611ca8 | |||
6b831bc367 | |||
ffd01fe30b | |||
8774dc89e7 | |||
16f183c117 | |||
ba80c1591d | |||
4fada9a8d2 | |||
7441892235 | |||
f929758bfd | |||
500f295506 | |||
de1f37735b | |||
9f9bbfa022 | |||
d516c5ebfc | |||
3668c1b3da | |||
0e3307b9f4 | |||
3ea16944d2 | |||
dcfcee9a23 | |||
fd451fe9d2 | |||
cc283c0be9 | |||
c9da64107f | |||
5b92ea0660 | |||
facc72b22d | |||
578c0f0ddc | |||
e099781686 | |||
3dfa587739 | |||
0d9f4a5f03 | |||
ab8faf7c99 | |||
7c3f3070f9 | |||
1bab65bb13 | |||
01063c2623 | |||
bd63013524 | |||
af62fbaef4 | |||
f41e0bda82 | |||
0d8dcc41b2 | |||
d179cd896f | |||
1f10f0f09c | |||
edb5f94b7c | |||
d563a85092 | |||
394f03ee1c | |||
7214b7c474 | |||
1b7e7c74f5 | |||
11a0cd79bb | |||
5f799fe1a1 | |||
c717a55c2d | |||
9a008d4623 | |||
47d268335e | |||
f136a02240 | |||
a33d1e12d2 | |||
337e8cd1d3 | |||
adfe26aad1 | |||
f84892dc6a | |||
c290f9fcca | |||
d85c4ec84b | |||
bce5d0d878 | |||
a3c17ea8ea | |||
982c6fbfab | |||
709963fd81 | |||
1f4f9a9c3e | |||
b621a1b327 | |||
d28be04416 | |||
db08272416 |
6
.editorconfig
Normal file
6
.editorconfig
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
45
.eslintrc.js
Normal file
45
.eslintrc.js
Normal 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',
|
||||||
|
],
|
||||||
|
}
|
|
@ -1,35 +0,0 @@
|
||||||
env:
|
|
||||||
es6: true
|
|
||||||
node: true
|
|
||||||
extends: 'eslint:recommended'
|
|
||||||
parser: 'babel-eslint'
|
|
||||||
parserOptions:
|
|
||||||
sourceType: module
|
|
||||||
ecmaVersion: 2017 # For async/await
|
|
||||||
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
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -18,3 +18,4 @@ www/js/cytube-google-drive.user.js
|
||||||
www/js/cytube-google-drive.meta.js
|
www/js/cytube-google-drive.meta.js
|
||||||
www/js/player.js
|
www/js/player.js
|
||||||
tor-exit-list.json
|
tor-exit-list.json
|
||||||
|
*.patch
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
Copyright (c) 2013-2021 Calvin Montgomery and contributors
|
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:
|
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:
|
||||||
|
|
||||||
|
|
67
NEWS.md
67
NEWS.md
|
@ -1,3 +1,70 @@
|
||||||
|
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
|
2020-08-21
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
176
bin/admin.js
Executable file
176
bin/admin.js
Executable 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);
|
||||||
|
}
|
|
@ -6,26 +6,35 @@ var path = require('path');
|
||||||
|
|
||||||
var order = [
|
var order = [
|
||||||
'base.coffee',
|
'base.coffee',
|
||||||
|
|
||||||
|
'dailymotion.coffee',
|
||||||
|
'niconico.coffee',
|
||||||
|
'peertube.coffee',
|
||||||
|
'soundcloud.coffee',
|
||||||
|
'twitch.coffee',
|
||||||
'vimeo.coffee',
|
'vimeo.coffee',
|
||||||
'youtube.coffee',
|
'youtube.coffee',
|
||||||
'dailymotion.coffee',
|
|
||||||
'videojs.coffee',
|
// playerjs-based players
|
||||||
'playerjs.coffee',
|
'playerjs.coffee',
|
||||||
|
'iframechild.coffee',
|
||||||
|
'odysee.coffee',
|
||||||
'streamable.coffee',
|
'streamable.coffee',
|
||||||
'gdrive-player.coffee',
|
|
||||||
'raw-file.coffee',
|
// iframe embed-based players
|
||||||
'soundcloud.coffee',
|
|
||||||
'embed.coffee',
|
'embed.coffee',
|
||||||
'twitch.coffee',
|
|
||||||
'livestream.com.coffee',
|
|
||||||
'custom-embed.coffee',
|
'custom-embed.coffee',
|
||||||
'rtmp.coffee',
|
'livestream.com.coffee',
|
||||||
'smashcast.coffee',
|
|
||||||
'ustream.coffee',
|
|
||||||
'imgur.coffee',
|
|
||||||
'gdrive-youtube.coffee',
|
|
||||||
'hls.coffee',
|
|
||||||
'twitchclip.coffee',
|
'twitchclip.coffee',
|
||||||
|
|
||||||
|
// video.js-based players
|
||||||
|
'videojs.coffee',
|
||||||
|
'gdrive-player.coffee',
|
||||||
|
'hls.coffee',
|
||||||
|
'raw-file.coffee',
|
||||||
|
'rtmp.coffee',
|
||||||
|
|
||||||
|
// mediaUpdate handler
|
||||||
'update.coffee'
|
'update.coffee'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,10 @@ io:
|
||||||
default-port: 1337
|
default-port: 1337
|
||||||
# limit the number of concurrent socket connections per IP address
|
# limit the number of concurrent socket connections per IP address
|
||||||
ip-connection-limit: 10
|
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
|
# YouTube v3 API key
|
||||||
# 1. Go to https://console.developers.google.com/, create a new "project" (or choose an existing one)
|
# 1. Go to https://console.developers.google.com/, create a new "project" (or choose an existing one)
|
||||||
|
@ -124,6 +128,8 @@ max-channels-per-user: 5
|
||||||
max-accounts-per-ip: 5
|
max-accounts-per-ip: 5
|
||||||
# Minimum number of seconds between guest logins from the same IP
|
# Minimum number of seconds between guest logins from the same IP
|
||||||
guest-login-delay: 60
|
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
|
# Allows you to customize the path divider. The /r/ in http://localhost/r/yourchannel
|
||||||
# Acceptable characters are a-z A-Z 0-9 _ and -
|
# Acceptable characters are a-z A-Z 0-9 _ and -
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
CyTube Custom Content Metadata
|
CyTube Custom Content Metadata
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
*Last updated: 2019-05-05*
|
*Last updated: 2022-02-12*
|
||||||
|
|
||||||
## Purpose ##
|
## Purpose ##
|
||||||
|
|
||||||
|
@ -61,6 +61,8 @@ To add custom content, the user provides a JSON object with the following keys:
|
||||||
playlist, but this functionality may be offered in the future.
|
playlist, but this functionality may be offered in the future.
|
||||||
* `sources`: A nonempty list of playable sources for the content. The format
|
* `sources`: A nonempty list of playable sources for the content. The format
|
||||||
is described below.
|
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
|
* `textTracks`: An optional list of text tracks for subtitles or closed
|
||||||
captioning. The format is described below.
|
captioning. The format is described below.
|
||||||
|
|
||||||
|
@ -99,19 +101,46 @@ The following MIME types are accepted for the `contentType` field:
|
||||||
RTMP streams are only supported through the existing `rt:` media
|
RTMP streams are only supported through the existing `rt:` media
|
||||||
type.
|
type.
|
||||||
* `audio/aac`
|
* `audio/aac`
|
||||||
* `audio/ogg`
|
* `audio/mp4`
|
||||||
* `audio/mpeg`
|
* `audio/mpeg`
|
||||||
|
* `audio/ogg`
|
||||||
|
|
||||||
Other audio or video formats, such as AVI, MKV, and FLAC, are not supported due
|
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
|
to lack of common support across browsers for playing these formats. For more
|
||||||
information, refer to
|
information, refer to
|
||||||
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility).
|
[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 ###
|
### Text Track Format ###
|
||||||
|
|
||||||
Each text track entry is a JSON object with the following keys:
|
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
|
* `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.
|
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`.
|
* `contentType`: A string representing the MIME type of the track at `url`.
|
||||||
|
@ -177,3 +206,5 @@ non-exhaustive.
|
||||||
to the browser.
|
to the browser.
|
||||||
* The manifest includes source URLs or text track URLs with expiration times,
|
* The manifest includes source URLs or text track URLs with expiration times,
|
||||||
session IDs, etc. in the URL querystring.
|
session IDs, etc. in the URL querystring.
|
||||||
|
* The manifest provides source URLs with non-silent audio as well as a list
|
||||||
|
of audioTracks.
|
||||||
|
|
|
@ -29,7 +29,9 @@ natively. Accordingly, CyTube only supports a few codecs:
|
||||||
|
|
||||||
**Video**
|
**Video**
|
||||||
|
|
||||||
|
* MP4 (AV1)
|
||||||
* MP4 (H.264)
|
* MP4 (H.264)
|
||||||
|
* WebM (AV1)
|
||||||
* WebM (VP8)
|
* WebM (VP8)
|
||||||
* WebM (VP9)
|
* WebM (VP9)
|
||||||
* Ogg/Theora
|
* Ogg/Theora
|
||||||
|
|
|
@ -21,7 +21,6 @@ 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.
|
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.
|
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.
|
||||||
Set wmode=transparent | There's probably no reason to touch this unless you know what you're doing. Having a non-transparent wmode can cause modals to display behind the video player, but also can cause performance issues in some situations.
|
|
||||||
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.
|
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.
|
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.
|
Old style playlist buttons | Legacy feature introduced in CyTube 2.0 for those who preferred the old 1.0-style video control buttons.
|
||||||
|
|
|
@ -110,6 +110,25 @@ describe('KickbanModule', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 => {
|
it('rejects if the user does not have ban permission', done => {
|
||||||
mockUser.socket.emit = (frame, obj) => {
|
mockUser.socket.emit = (frame, obj) => {
|
||||||
if (frame === 'errorMsg') {
|
if (frame === 'errorMsg') {
|
||||||
|
|
109
integration_test/controller/banned-channels.js
Normal file
109
integration_test/controller/banned-channels.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
10975
package-lock.json
generated
10975
package-lock.json
generated
File diff suppressed because it is too large
Load diff
42
package.json
42
package.json
|
@ -2,39 +2,40 @@
|
||||||
"author": "Calvin Montgomery",
|
"author": "Calvin Montgomery",
|
||||||
"name": "CyTube",
|
"name": "CyTube",
|
||||||
"description": "Online media synchronizer and chat",
|
"description": "Online media synchronizer and chat",
|
||||||
"version": "3.76.3",
|
"version": "3.86.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "http://github.com/calzoneman/sync"
|
"url": "http://github.com/calzoneman/sync"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calzoneman/jsli": "^2.0.1",
|
"@calzoneman/jsli": "^2.0.1",
|
||||||
"@cytube/mediaquery": "0.0.25",
|
"@cytube/mediaquery": "github:CyTube/mediaquery#564d0c4615e80f72722b0f68ac81f837a4c5fc81",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.20.1",
|
||||||
"cheerio": "^1.0.0-rc.5",
|
"cheerio": "^1.0.0-rc.10",
|
||||||
"clone": "^2.1.2",
|
"clone": "^2.1.2",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"create-error": "^0.3.1",
|
"create-error": "^0.3.1",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"cytubefilters": "github:calzoneman/cytubefilters#c67b2dab2dc5cc5ed11018819f71273d0f8a1bf5",
|
"cytubefilters": "github:calzoneman/cytubefilters#c67b2dab2dc5cc5ed11018819f71273d0f8a1bf5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.18.2",
|
||||||
"express-minify": "^1.0.0",
|
"express-minify": "^1.0.0",
|
||||||
"json-typecheck": "^0.1.3",
|
"json-typecheck": "^0.1.3",
|
||||||
"knex": "^0.95.2",
|
"knex": "^2.4.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"mysql": "^2.18.1",
|
"nodemailer": "^6.6.1",
|
||||||
"nodemailer": "^6.5.0",
|
"pg": "^8.11.3",
|
||||||
|
"pg-native": "^3.0.1",
|
||||||
"prom-client": "^13.1.0",
|
"prom-client": "^13.1.0",
|
||||||
"proxy-addr": "^2.0.6",
|
"proxy-addr": "^2.0.6",
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
"redis": "^3.0.2",
|
"redis": "^3.1.1",
|
||||||
"sanitize-html": "^2.3.3",
|
"sanitize-html": "^2.7.0",
|
||||||
"serve-static": "^1.14.1",
|
"serve-static": "^1.15.0",
|
||||||
"socket.io": "^2.0.3",
|
"socket.io": "^4.5.4",
|
||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.19",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
|
@ -53,15 +54,16 @@
|
||||||
"integration-test": "mocha --recursive --exit integration_test"
|
"integration-test": "mocha --recursive --exit integration_test"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.10.5",
|
"@babel/cli": "^7.15.7",
|
||||||
"@babel/core": "^7.11.4",
|
"@babel/core": "^7.15.8",
|
||||||
"@babel/preset-env": "^7.11.0",
|
"@babel/eslint-parser": "^7.15.8",
|
||||||
"babel-eslint": "^10.1.0",
|
"@babel/preset-env": "^7.15.8",
|
||||||
"babel-plugin-add-module-exports": "^1.0.2",
|
"babel-plugin-add-module-exports": "^1.0.4",
|
||||||
"coffeescript": "^1.9.2",
|
"coffeescript": "^1.9.2",
|
||||||
"eslint": "^7.7.0",
|
"eslint": "^7.32.0",
|
||||||
"mocha": "^8.1.1",
|
"eslint-plugin-no-jquery": "^2.7.0",
|
||||||
"sinon": "^9.0.3"
|
"mocha": "^9.2.2",
|
||||||
|
"sinon": "^10.0.0"
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
|
|
|
@ -15,13 +15,24 @@ window.CustomEmbedPlayer = class CustomEmbedPlayer extends EmbedPlayer
|
||||||
return
|
return
|
||||||
|
|
||||||
embedSrc = data.meta.embed.src
|
embedSrc = data.meta.embed.src
|
||||||
link = "<a href=\"#{embedSrc}\" target=\"_blank\"><strong>#{embedSrc}</strong></a>"
|
|
||||||
alert = makeAlert('Untrusted Content', CUSTOM_EMBED_WARNING.replace('%link%', link),
|
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')
|
'alert-warning')
|
||||||
.removeClass('col-md-12')
|
.removeClass('col-md-12')
|
||||||
$('<button/>').addClass('btn btn-default')
|
$('<button/>').addClass('btn btn-default')
|
||||||
.text('Embed')
|
.text('Embed')
|
||||||
.click(=>
|
.on('click', =>
|
||||||
super(data)
|
super(data)
|
||||||
)
|
)
|
||||||
.appendTo(alert.find('.alert'))
|
.appendTo(alert.find('.alert'))
|
||||||
|
|
|
@ -12,7 +12,6 @@ window.DailymotionPlayer = class DailymotionPlayer extends Player
|
||||||
|
|
||||||
params =
|
params =
|
||||||
autoplay: 1
|
autoplay: 1
|
||||||
wmode: if USEROPTS.wmode_transparent then 'transparent' else 'opaque'
|
|
||||||
logo: 0
|
logo: 0
|
||||||
|
|
||||||
quality = @mapQuality(USEROPTS.default_quality)
|
quality = @mapQuality(USEROPTS.default_quality)
|
||||||
|
|
|
@ -24,27 +24,10 @@ window.EmbedPlayer = class EmbedPlayer extends Player
|
||||||
console.error('EmbedPlayer::load(): missing meta.embed')
|
console.error('EmbedPlayer::load(): missing meta.embed')
|
||||||
return
|
return
|
||||||
|
|
||||||
if embed.tag == 'object'
|
@player = @loadIframe(embed)
|
||||||
@player = @loadObject(embed)
|
|
||||||
else
|
|
||||||
@player = @loadIframe(embed)
|
|
||||||
|
|
||||||
removeOld(@player)
|
removeOld(@player)
|
||||||
|
|
||||||
loadObject: (embed) ->
|
|
||||||
object = $('<object/>').attr(
|
|
||||||
type: 'application/x-shockwave-flash'
|
|
||||||
data: embed.src
|
|
||||||
wmode: 'opaque'
|
|
||||||
)
|
|
||||||
genParam('allowfullscreen', 'true').appendTo(object)
|
|
||||||
genParam('allowscriptaccess', 'always').appendTo(object)
|
|
||||||
|
|
||||||
for key, value of embed.params
|
|
||||||
genParam(key, value).appendTo(object)
|
|
||||||
|
|
||||||
return object
|
|
||||||
|
|
||||||
loadIframe: (embed) ->
|
loadIframe: (embed) ->
|
||||||
if embed.src.indexOf('http:') == 0 and location.protocol == 'https:'
|
if embed.src.indexOf('http:') == 0 and location.protocol == 'https:'
|
||||||
if @__proto__.mixedContentError?
|
if @__proto__.mixedContentError?
|
||||||
|
|
|
@ -31,3 +31,56 @@ window.GoogleDrivePlayer = class GoogleDrivePlayer extends VideoJSPlayer
|
||||||
jitter: 500
|
jitter: 500
|
||||||
})
|
})
|
||||||
, Math.random() * 1000)
|
, 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 = '×'
|
||||||
|
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 = '×'
|
||||||
|
closeButton.onclick = ->
|
||||||
|
alertBox.parentNode.removeChild(alertBox)
|
||||||
|
alertBox.insertBefore(closeButton, alertBox.firstChild)
|
||||||
|
removeOld($('<div/>').append(alertBox))
|
||||||
|
|
|
@ -1,131 +0,0 @@
|
||||||
window.GoogleDriveYouTubePlayer = class GoogleDriveYouTubePlayer extends Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof GoogleDriveYouTubePlayer)
|
|
||||||
return new GoogleDriveYouTubePlayer(data)
|
|
||||||
|
|
||||||
@setMediaProperties(data)
|
|
||||||
@init(data)
|
|
||||||
|
|
||||||
init: (data) ->
|
|
||||||
window.promptToInstallDriveUserscript()
|
|
||||||
embed = $('<embed />').attr(
|
|
||||||
type: 'application/x-shockwave-flash'
|
|
||||||
src: "https://www.youtube.com/get_player?docid=#{data.id}&ps=docs\
|
|
||||||
&partnerid=30&enablejsapi=1&cc_load_policy=1\
|
|
||||||
&auth_timeout=86400000000"
|
|
||||||
flashvars: 'autoplay=1&playerapiid=uniquePlayerId'
|
|
||||||
wmode: 'opaque'
|
|
||||||
allowscriptaccess: 'always'
|
|
||||||
)
|
|
||||||
removeOld(embed)
|
|
||||||
|
|
||||||
window.onYouTubePlayerReady = =>
|
|
||||||
if PLAYER != this
|
|
||||||
return
|
|
||||||
|
|
||||||
@yt = embed[0]
|
|
||||||
window.gdriveStateChange = @onStateChange.bind(this)
|
|
||||||
@yt.addEventListener('onStateChange', 'gdriveStateChange')
|
|
||||||
@onReady()
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@yt = null
|
|
||||||
@setMediaProperties(data)
|
|
||||||
@init(data)
|
|
||||||
|
|
||||||
onReady: ->
|
|
||||||
@yt.ready = true
|
|
||||||
@setVolume(VOLUME)
|
|
||||||
@setQuality(USEROPTS.default_quality)
|
|
||||||
|
|
||||||
onStateChange: (ev) ->
|
|
||||||
if PLAYER != this
|
|
||||||
return
|
|
||||||
|
|
||||||
if (ev == YT.PlayerState.PAUSED and not @paused) or
|
|
||||||
(ev == YT.PlayerState.PLAYING and @paused)
|
|
||||||
@paused = (ev == YT.PlayerState.PAUSED)
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
|
|
||||||
if ev == 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) ->
|
|
||||||
if not @yt or not @yt.ready
|
|
||||||
return
|
|
||||||
|
|
||||||
ytQuality = switch String(quality)
|
|
||||||
when '240' then 'small'
|
|
||||||
when '360' then 'medium'
|
|
||||||
when '480' then 'large'
|
|
||||||
when '720' then 'hd720'
|
|
||||||
when '1080' then 'hd1080'
|
|
||||||
when 'best' then 'highres'
|
|
||||||
else 'auto'
|
|
||||||
|
|
||||||
if ytQuality != 'auto'
|
|
||||||
@yt.setPlaybackQuality(ytQuality)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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 = '×'
|
|
||||||
closeButton.onclick = ->
|
|
||||||
alertBox.parentNode.removeChild(alertBox)
|
|
||||||
alertBox.insertBefore(closeButton, alertBox.firstChild)
|
|
||||||
removeOld($('<div/>').append(alertBox))
|
|
33
player/iframechild.coffee
Normal file
33
player/iframechild.coffee
Normal 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)
|
||||||
|
)
|
|
@ -1,12 +0,0 @@
|
||||||
window.ImgurPlayer = class ImgurPlayer extends EmbedPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof ImgurPlayer)
|
|
||||||
return new ImgurPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
data.meta.embed =
|
|
||||||
tag: 'iframe'
|
|
||||||
src: "https://imgur.com/a/#{data.id}/embed"
|
|
||||||
super(data)
|
|
|
@ -6,18 +6,12 @@ window.LivestreamPlayer = class LivestreamPlayer extends EmbedPlayer
|
||||||
@load(data)
|
@load(data)
|
||||||
|
|
||||||
load: (data) ->
|
load: (data) ->
|
||||||
if LIVESTREAM_CHROMELESS
|
[ account, event ] = data.id.split(';')
|
||||||
data.meta.embed =
|
data.meta.embed =
|
||||||
src: 'https://cdn.livestream.com/chromelessPlayer/v20/playerapi.swf'
|
src: "https://livestream.com/accounts/#{account}/events/#{event}/player?\
|
||||||
tag: 'object'
|
enableInfoAndActivity=false&\
|
||||||
params:
|
defaultDrawer=&\
|
||||||
flashvars: "channel=#{data.id}"
|
autoPlay=true&\
|
||||||
else
|
mute=false"
|
||||||
data.meta.embed =
|
tag: 'iframe'
|
||||||
src: "https://cdn.livestream.com/embed/#{data.id}?\
|
|
||||||
layout=4&\
|
|
||||||
color=0x000000&\
|
|
||||||
iconColorOver=0xe7e7e7&\
|
|
||||||
iconColor=0xcccccc"
|
|
||||||
tag: 'iframe'
|
|
||||||
super(data)
|
super(data)
|
||||||
|
|
66
player/niconico.coffee
Normal file
66
player/niconico.coffee
Normal 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
21
player/odysee.coffee
Normal 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
122
player/peertube.coffee
Normal 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)
|
||||||
|
|
|
@ -8,55 +8,48 @@ window.PlayerJSPlayer = class PlayerJSPlayer extends Player
|
||||||
load: (data) ->
|
load: (data) ->
|
||||||
@setMediaProperties(data)
|
@setMediaProperties(data)
|
||||||
@ready = false
|
@ready = false
|
||||||
@finishing = false
|
|
||||||
|
|
||||||
if not data.meta.playerjs
|
if not data.meta.playerjs
|
||||||
throw new Error('Invalid input: missing meta.playerjs')
|
throw new Error('Invalid input: missing meta.playerjs')
|
||||||
|
|
||||||
waitUntilDefined(window, 'playerjs', =>
|
waitUntilDefined(window, 'playerjs', =>
|
||||||
iframe = $('<iframe/>')
|
iframe = $('<iframe/>')
|
||||||
.attr(src: data.meta.playerjs.src)
|
.attr(
|
||||||
|
src: data.meta.playerjs.src
|
||||||
|
allow: 'autoplay; fullscreen'
|
||||||
|
)
|
||||||
|
|
||||||
removeOld(iframe)
|
removeOld(iframe)
|
||||||
|
@setupPlayer(iframe[0])
|
||||||
|
)
|
||||||
|
|
||||||
@player = new playerjs.Player(iframe[0])
|
setupPlayer: (iframe) ->
|
||||||
@player.on('ready', =>
|
@player = new playerjs.Player(iframe)
|
||||||
@player.on('error', (error) =>
|
@player.on('ready', =>
|
||||||
console.error('PlayerJS error', error.stack)
|
@player.on('error', (error) =>
|
||||||
)
|
console.error('PlayerJS error', error.stack)
|
||||||
@player.on('ended', ->
|
|
||||||
# Streamable seems to not implement this since it loops
|
|
||||||
# gotta use the timeupdate hack below
|
|
||||||
if CLIENT.leader
|
|
||||||
socket.emit('playNext')
|
|
||||||
)
|
|
||||||
@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
|
|
||||||
)
|
|
||||||
@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
|
|
||||||
)
|
)
|
||||||
|
@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: ->
|
play: ->
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
codecToMimeType = (codec) ->
|
codecToMimeType = (codec) ->
|
||||||
switch codec
|
switch codec
|
||||||
when 'mov/h264' then 'video/mp4'
|
when 'mov/h264', 'mov/av1' then 'video/mp4'
|
||||||
when 'flv/h264' then 'video/flv'
|
when 'flv/h264' then 'video/flv'
|
||||||
when 'matroska/vp8', 'matroska/vp9' then 'video/webm'
|
when 'matroska/vp8', 'matroska/vp9', 'matroska/av1' then 'video/webm'
|
||||||
when 'ogg/theora' then 'video/ogg'
|
when 'ogg/theora' then 'video/ogg'
|
||||||
when 'mp3' then 'audio/mp3'
|
when 'mp3' then 'audio/mp3'
|
||||||
when 'vorbis' then 'audio/ogg'
|
when 'vorbis' then 'audio/ogg'
|
||||||
when 'aac' then 'audio/aac'
|
when 'aac' then 'audio/aac'
|
||||||
|
when 'opus' then 'audio/opus'
|
||||||
else 'video/flv'
|
else 'video/flv'
|
||||||
|
|
||||||
window.FilePlayer = class FilePlayer extends VideoJSPlayer
|
window.FilePlayer = class FilePlayer extends VideoJSPlayer
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
window.SmashcastPlayer = class SmashcastPlayer extends EmbedPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof SmashcastPlayer)
|
|
||||||
return new SmashcastPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
data.meta.embed =
|
|
||||||
src: "https://www.smashcast.tv/embed/#{data.id}"
|
|
||||||
tag: 'iframe'
|
|
||||||
super(data)
|
|
|
@ -6,7 +6,30 @@ window.StreamablePlayer = class StreamablePlayer extends PlayerJSPlayer
|
||||||
super(data)
|
super(data)
|
||||||
|
|
||||||
load: (data) ->
|
load: (data) ->
|
||||||
data.meta.playerjs =
|
@ready = false
|
||||||
src: "https://streamable.com/e/#{data.id}"
|
@finishing = false
|
||||||
|
@setMediaProperties(data)
|
||||||
|
|
||||||
super(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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -3,7 +3,6 @@ TYPE_MAP =
|
||||||
vi: VimeoPlayer
|
vi: VimeoPlayer
|
||||||
dm: DailymotionPlayer
|
dm: DailymotionPlayer
|
||||||
gd: GoogleDrivePlayer
|
gd: GoogleDrivePlayer
|
||||||
gp: VideoJSPlayer
|
|
||||||
fi: FilePlayer
|
fi: FilePlayer
|
||||||
sc: SoundCloudPlayer
|
sc: SoundCloudPlayer
|
||||||
li: LivestreamPlayer
|
li: LivestreamPlayer
|
||||||
|
@ -11,13 +10,15 @@ TYPE_MAP =
|
||||||
tv: TwitchPlayer
|
tv: TwitchPlayer
|
||||||
cu: CustomEmbedPlayer
|
cu: CustomEmbedPlayer
|
||||||
rt: RTMPPlayer
|
rt: RTMPPlayer
|
||||||
hb: SmashcastPlayer
|
|
||||||
us: UstreamPlayer
|
|
||||||
im: ImgurPlayer
|
|
||||||
hl: HLSPlayer
|
hl: HLSPlayer
|
||||||
sb: StreamablePlayer
|
sb: StreamablePlayer
|
||||||
tc: TwitchClipPlayer
|
tc: TwitchClipPlayer
|
||||||
cm: VideoJSPlayer
|
cm: VideoJSPlayer
|
||||||
|
pt: PeerPlayer
|
||||||
|
bc: IframeChild
|
||||||
|
bn: IframeChild
|
||||||
|
od: OdyseePlayer
|
||||||
|
nv: NicoPlayer
|
||||||
|
|
||||||
window.loadMediaPlayer = (data) ->
|
window.loadMediaPlayer = (data) ->
|
||||||
try
|
try
|
||||||
|
@ -109,7 +110,8 @@ window.removeOld = (replace) ->
|
||||||
$('#soundcloud-volume-holder').remove()
|
$('#soundcloud-volume-holder').remove()
|
||||||
replace ?= $('<div/>').addClass('embed-responsive-item')
|
replace ?= $('<div/>').addClass('embed-responsive-item')
|
||||||
old = $('#ytapiplayer')
|
old = $('#ytapiplayer')
|
||||||
|
old.attr('id', 'ytapiplayer-old')
|
||||||
|
replace.attr('id', 'ytapiplayer')
|
||||||
replace.insertBefore(old)
|
replace.insertBefore(old)
|
||||||
old.remove()
|
old.remove()
|
||||||
replace.attr('id', 'ytapiplayer')
|
|
||||||
return replace
|
return replace
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
window.UstreamPlayer = class UstreamPlayer extends EmbedPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof UstreamPlayer)
|
|
||||||
return new UstreamPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
data.meta.embed =
|
|
||||||
tag: 'iframe'
|
|
||||||
src: "https://www.ustream.tv/embed/#{data.id}?html5ui"
|
|
||||||
super(data)
|
|
|
@ -42,9 +42,13 @@ getSourceLabel = (source) ->
|
||||||
else
|
else
|
||||||
return "#{source.quality}p #{source.contentType.split('/')[1]}"
|
return "#{source.quality}p #{source.contentType.split('/')[1]}"
|
||||||
|
|
||||||
waitUntilDefined(window, 'videojs', =>
|
hasAnyTextTracks = (data) ->
|
||||||
videojs.options.flash.swf = '/video-js.swf'
|
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
|
window.VideoJSPlayer = class VideoJSPlayer extends Player
|
||||||
constructor: (data) ->
|
constructor: (data) ->
|
||||||
|
@ -59,7 +63,7 @@ window.VideoJSPlayer = class VideoJSPlayer extends Player
|
||||||
width: '100%'
|
width: '100%'
|
||||||
height: '100%'
|
height: '100%'
|
||||||
|
|
||||||
if @mediaType == 'cm' and data.meta.textTracks
|
if @mediaType == 'cm' and hasAnyTextTracks(data)
|
||||||
attrs.crossorigin = 'anonymous'
|
attrs.crossorigin = 'anonymous'
|
||||||
|
|
||||||
video = $('<video/>')
|
video = $('<video/>')
|
||||||
|
@ -108,17 +112,26 @@ window.VideoJSPlayer = class VideoJSPlayer extends Player
|
||||||
$('<track/>').attr(attrs).appendTo(video)
|
$('<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],
|
@player = videojs(video[0],
|
||||||
# https://github.com/Dash-Industry-Forum/dash.js/issues/2184
|
# https://github.com/Dash-Industry-Forum/dash.js/issues/2184
|
||||||
autoplay: @sources[0].type != 'application/dash+xml',
|
autoplay: @sources[0].type != 'application/dash+xml',
|
||||||
controls: true,
|
controls: true,
|
||||||
plugins:
|
plugins: pluginData
|
||||||
videoJsResolutionSwitcher:
|
|
||||||
default: @sources[0].res
|
|
||||||
)
|
)
|
||||||
@player.ready(=>
|
@player.ready(=>
|
||||||
# Have to use updateSrc instead of <source> tags
|
# Have to use updateSrc instead of <source> tags
|
||||||
# see: https://github.com/videojs/video.js/issues/3428
|
# see: https://github.com/videojs/video.js/issues/3428
|
||||||
|
@player.poster(data.meta.thumbnail)
|
||||||
@player.updateSrc(@sources)
|
@player.updateSrc(@sources)
|
||||||
@player.on('error', =>
|
@player.on('error', =>
|
||||||
err = @player.error()
|
err = @player.error()
|
||||||
|
@ -130,8 +143,11 @@ window.VideoJSPlayer = class VideoJSPlayer extends Player
|
||||||
@player.src(@sources[@sourceIdx])
|
@player.src(@sources[@sourceIdx])
|
||||||
else
|
else
|
||||||
console.error('Out of sources, video will not play')
|
console.error('Out of sources, video will not play')
|
||||||
if @mediaType is 'gd' and not window.hasDriveUserscript
|
if @mediaType is 'gd'
|
||||||
window.promptToInstallDriveUserscript()
|
if not window.hasDriveUserscript
|
||||||
|
window.promptToInstallDriveUserscript()
|
||||||
|
else
|
||||||
|
window.tellUserNotToContactMeAboutThingsThatAreNotSupported()
|
||||||
)
|
)
|
||||||
@setVolume(VOLUME)
|
@setVolume(VOLUME)
|
||||||
@player.on('ended', ->
|
@player.on('ended', ->
|
||||||
|
|
|
@ -13,14 +13,9 @@ window.VimeoPlayer = class VimeoPlayer extends Player
|
||||||
removeOld(video)
|
removeOld(video)
|
||||||
video.attr(
|
video.attr(
|
||||||
src: "https://player.vimeo.com/video/#{data.id}"
|
src: "https://player.vimeo.com/video/#{data.id}"
|
||||||
webkitallowfullscreen: true
|
allow: 'autoplay; fullscreen'
|
||||||
mozallowfullscreen: true
|
|
||||||
allowfullscreen: true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if USEROPTS.wmode_transparent
|
|
||||||
video.attr('wmode', 'transparent')
|
|
||||||
|
|
||||||
@vimeo = new Vimeo.Player(video[0])
|
@vimeo = new Vimeo.Player(video[0])
|
||||||
|
|
||||||
@vimeo.on('ended', =>
|
@vimeo.on('ended', =>
|
||||||
|
|
|
@ -4,7 +4,6 @@ window.YouTubePlayer = class YouTubePlayer extends Player
|
||||||
return new YouTubePlayer(data)
|
return new YouTubePlayer(data)
|
||||||
|
|
||||||
@setMediaProperties(data)
|
@setMediaProperties(data)
|
||||||
@qualityRaceCondition = true
|
|
||||||
@pauseSeekRaceCondition = false
|
@pauseSeekRaceCondition = false
|
||||||
|
|
||||||
waitUntilDefined(window, 'YT', =>
|
waitUntilDefined(window, 'YT', =>
|
||||||
|
@ -13,7 +12,6 @@ window.YouTubePlayer = class YouTubePlayer extends Player
|
||||||
waitUntilDefined(YT, 'Player', =>
|
waitUntilDefined(YT, 'Player', =>
|
||||||
removeOld()
|
removeOld()
|
||||||
|
|
||||||
wmode = if USEROPTS.wmode_transparent then 'transparent' else 'opaque'
|
|
||||||
@yt = new YT.Player('ytapiplayer',
|
@yt = new YT.Player('ytapiplayer',
|
||||||
videoId: data.id
|
videoId: data.id
|
||||||
playerVars:
|
playerVars:
|
||||||
|
@ -22,7 +20,6 @@ window.YouTubePlayer = class YouTubePlayer extends Player
|
||||||
controls: 1
|
controls: 1
|
||||||
iv_load_policy: 3 # iv_load_policy 3 indicates no annotations
|
iv_load_policy: 3 # iv_load_policy 3 indicates no annotations
|
||||||
rel: 0
|
rel: 0
|
||||||
wmode: wmode
|
|
||||||
events:
|
events:
|
||||||
onReady: @onReady.bind(this)
|
onReady: @onReady.bind(this)
|
||||||
onStateChange: @onStateChange.bind(this)
|
onStateChange: @onStateChange.bind(this)
|
||||||
|
@ -34,9 +31,6 @@ window.YouTubePlayer = class YouTubePlayer extends Player
|
||||||
@setMediaProperties(data)
|
@setMediaProperties(data)
|
||||||
if @yt and @yt.ready
|
if @yt and @yt.ready
|
||||||
@yt.loadVideoById(data.id, data.currentTime)
|
@yt.loadVideoById(data.id, data.currentTime)
|
||||||
@qualityRaceCondition = true
|
|
||||||
if USEROPTS.default_quality
|
|
||||||
@setQuality(USEROPTS.default_quality)
|
|
||||||
else
|
else
|
||||||
console.error('WTF? YouTubePlayer::load() called but yt is not ready')
|
console.error('WTF? YouTubePlayer::load() called but yt is not ready')
|
||||||
|
|
||||||
|
@ -45,15 +39,9 @@ window.YouTubePlayer = class YouTubePlayer extends Player
|
||||||
@setVolume(VOLUME)
|
@setVolume(VOLUME)
|
||||||
|
|
||||||
onStateChange: (ev) ->
|
onStateChange: (ev) ->
|
||||||
# For some reason setting the quality doesn't work
|
# If you pause the video before the first PLAYING
|
||||||
# until the first event has fired.
|
# event is emitted, weird things happen (or at least that was true
|
||||||
if @qualityRaceCondition
|
# whenever this comment was authored in 2015).
|
||||||
@qualityRaceCondition = false
|
|
||||||
if USEROPTS.default_quality
|
|
||||||
@setQuality(USEROPTS.default_quality)
|
|
||||||
|
|
||||||
# Similar to above, if you pause the video before the first PLAYING
|
|
||||||
# event is emitted, weird things happen.
|
|
||||||
if ev.data == YT.PlayerState.PLAYING and @pauseSeekRaceCondition
|
if ev.data == YT.PlayerState.PLAYING and @pauseSeekRaceCondition
|
||||||
@pause()
|
@pause()
|
||||||
@pauseSeekRaceCondition = false
|
@pauseSeekRaceCondition = false
|
||||||
|
@ -90,20 +78,7 @@ window.YouTubePlayer = class YouTubePlayer extends Player
|
||||||
@yt.setVolume(volume * 100)
|
@yt.setVolume(volume * 100)
|
||||||
|
|
||||||
setQuality: (quality) ->
|
setQuality: (quality) ->
|
||||||
if not @yt or not @yt.ready
|
# https://github.com/calzoneman/sync/issues/726
|
||||||
return
|
|
||||||
|
|
||||||
ytQuality = switch String(quality)
|
|
||||||
when '240' then 'small'
|
|
||||||
when '360' then 'medium'
|
|
||||||
when '480' then 'large'
|
|
||||||
when '720' then 'hd720'
|
|
||||||
when '1080' then 'hd1080'
|
|
||||||
when 'best' then 'highres'
|
|
||||||
else 'auto'
|
|
||||||
|
|
||||||
if ytQuality != 'auto'
|
|
||||||
@yt.setPlaybackQuality(ytQuality)
|
|
||||||
|
|
||||||
getTime: (cb) ->
|
getTime: (cb) ->
|
||||||
if @yt and @yt.ready
|
if @yt and @yt.ready
|
||||||
|
|
1
src/.eslintrc.json
Normal file
1
src/.eslintrc.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{ "env": { "node": true } }
|
|
@ -94,7 +94,10 @@ function Channel(name) {
|
||||||
}, USERCOUNT_THROTTLE);
|
}, USERCOUNT_THROTTLE);
|
||||||
const self = this;
|
const self = this;
|
||||||
db.channels.load(this, function (err) {
|
db.channels.load(this, function (err) {
|
||||||
if (err && err !== "Channel is not registered") {
|
if (err && err.code === 'EBANNED') {
|
||||||
|
self.emit("loadFail", err.message);
|
||||||
|
self.setFlag(Flags.C_ERROR);
|
||||||
|
} else if (err && err !== "Channel is not registered") {
|
||||||
self.emit("loadFail", "Failed to load channel data from the database. Please try again later.");
|
self.emit("loadFail", "Failed to load channel data from the database. Please try again later.");
|
||||||
self.setFlag(Flags.C_ERROR);
|
self.setFlag(Flags.C_ERROR);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,7 +3,6 @@ var XSS = require("../xss");
|
||||||
var ChannelModule = require("./module");
|
var ChannelModule = require("./module");
|
||||||
var util = require("../utilities");
|
var util = require("../utilities");
|
||||||
var Flags = require("../flags");
|
var Flags = require("../flags");
|
||||||
var counters = require("../counters");
|
|
||||||
import { transformImgTags } from '../camo';
|
import { transformImgTags } from '../camo';
|
||||||
import { Counter } from 'prom-client';
|
import { Counter } from 'prom-client';
|
||||||
|
|
||||||
|
@ -157,14 +156,13 @@ const chatIncomingCount = new Counter({
|
||||||
});
|
});
|
||||||
ChatModule.prototype.handleChatMsg = function (user, data) {
|
ChatModule.prototype.handleChatMsg = function (user, data) {
|
||||||
var self = this;
|
var self = this;
|
||||||
counters.add("chat:incoming");
|
|
||||||
chatIncomingCount.inc(1, new Date());
|
chatIncomingCount.inc(1, new Date());
|
||||||
|
|
||||||
if (!this.channel || !this.channel.modules.permissions.canChat(user)) {
|
if (!this.channel || !this.channel.modules.permissions.canChat(user)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.msg = data.msg.substring(0, 320);
|
data.msg = data.msg.substring(0, Config.get("max-chat-message-length"));
|
||||||
|
|
||||||
// Restrict new accounts/IPs from chatting and posting links
|
// Restrict new accounts/IPs from chatting and posting links
|
||||||
if (this.restrictNewAccount(user, data)) {
|
if (this.restrictNewAccount(user, data)) {
|
||||||
|
@ -250,7 +248,7 @@ ChatModule.prototype.handlePm = function (user, data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
data.msg = data.msg.substring(0, 320);
|
data.msg = data.msg.substring(0, Config.get("max-chat-message-length"));
|
||||||
var to = null;
|
var to = null;
|
||||||
for (var i = 0; i < this.channel.users.length; i++) {
|
for (var i = 0; i < this.channel.users.length; i++) {
|
||||||
if (this.channel.users[i].getLowerName() === data.to) {
|
if (this.channel.users[i].getLowerName() === data.to) {
|
||||||
|
@ -358,7 +356,6 @@ ChatModule.prototype.processChatMsg = function (user, data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.sendMessage(msgobj);
|
this.sendMessage(msgobj);
|
||||||
counters.add("chat:sent");
|
|
||||||
chatSentCount.inc(1, new Date());
|
chatSentCount.inc(1, new Date());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ var Flags = require("../flags");
|
||||||
var util = require("../utilities");
|
var util = require("../utilities");
|
||||||
var Account = require("../account");
|
var Account = require("../account");
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
const XSS = require("../xss");
|
||||||
|
|
||||||
const dbIsNameBanned = Promise.promisify(db.channels.isNameBanned);
|
const dbIsNameBanned = Promise.promisify(db.channels.isNameBanned);
|
||||||
const dbIsIPBanned = Promise.promisify(db.channels.isIPBanned);
|
const dbIsIPBanned = Promise.promisify(db.channels.isIPBanned);
|
||||||
|
@ -261,7 +262,6 @@ KickBanModule.prototype.handleCmdIPBan = function (user, msg, _meta) {
|
||||||
chan.refCounter.ref("KickBanModule::handleCmdIPBan");
|
chan.refCounter.ref("KickBanModule::handleCmdIPBan");
|
||||||
|
|
||||||
this.banAll(user, name, range, reason).catch(error => {
|
this.banAll(user, name, range, reason).catch(error => {
|
||||||
//console.log('!!!', error.stack);
|
|
||||||
const message = error.message || error;
|
const message = error.message || error;
|
||||||
user.socket.emit("errorMsg", { msg: message });
|
user.socket.emit("errorMsg", { msg: message });
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -276,6 +276,10 @@ KickBanModule.prototype.checkChannelAlive = function checkChannelAlive() {
|
||||||
};
|
};
|
||||||
|
|
||||||
KickBanModule.prototype.banName = async function banName(actor, name, reason) {
|
KickBanModule.prototype.banName = async function banName(actor, name, reason) {
|
||||||
|
if (!util.isValidUserName(name)) {
|
||||||
|
throw new Error("Invalid username");
|
||||||
|
}
|
||||||
|
|
||||||
reason = reason.substring(0, 255);
|
reason = reason.substring(0, 255);
|
||||||
|
|
||||||
var chan = this.channel;
|
var chan = this.channel;
|
||||||
|
@ -323,6 +327,9 @@ KickBanModule.prototype.banName = async function banName(actor, name, reason) {
|
||||||
};
|
};
|
||||||
|
|
||||||
KickBanModule.prototype.banIP = async function banIP(actor, ip, name, reason) {
|
KickBanModule.prototype.banIP = async function banIP(actor, ip, name, reason) {
|
||||||
|
if (!util.isValidUserName(name)) {
|
||||||
|
throw new Error("Invalid username");
|
||||||
|
}
|
||||||
reason = reason.substring(0, 255);
|
reason = reason.substring(0, 255);
|
||||||
var masked = util.cloakIP(ip);
|
var masked = util.cloakIP(ip);
|
||||||
|
|
||||||
|
@ -404,7 +411,12 @@ KickBanModule.prototype.banAll = async function banAll(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!await dbIsNameBanned(chan.name, name)) {
|
if (!await dbIsNameBanned(chan.name, name)) {
|
||||||
promises.push(this.banName(actor, name, reason));
|
promises.push(this.banName(actor, name, reason).catch(error => {
|
||||||
|
// TODO: banning should be made idempotent, not throw an error
|
||||||
|
if (!/already banned/.test(error.message)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
@ -440,8 +452,9 @@ KickBanModule.prototype.handleUnban = function (user, data) {
|
||||||
self.channel.logger.log("[mod] " + user.getName() + " unbanned " + data.name);
|
self.channel.logger.log("[mod] " + user.getName() + " unbanned " + data.name);
|
||||||
if (self.channel.modules.chat) {
|
if (self.channel.modules.chat) {
|
||||||
var banperm = self.channel.modules.permissions.permissions.ban;
|
var banperm = self.channel.modules.permissions.permissions.ban;
|
||||||
|
// TODO: quick fix, shouldn't trust name from unban frame.
|
||||||
self.channel.modules.chat.sendModMessage(
|
self.channel.modules.chat.sendModMessage(
|
||||||
user.getName() + " unbanned " + data.name,
|
user.getName() + " unbanned " + XSS.sanitizeText(data.name),
|
||||||
banperm
|
banperm
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ var Flags = require("../flags");
|
||||||
var db = require("../database");
|
var db = require("../database");
|
||||||
var CustomEmbedFilter = require("../customembed").filter;
|
var CustomEmbedFilter = require("../customembed").filter;
|
||||||
var XSS = require("../xss");
|
var XSS = require("../xss");
|
||||||
import counters from '../counters';
|
|
||||||
import { Counter } from 'prom-client';
|
import { Counter } from 'prom-client';
|
||||||
|
|
||||||
const LOGGER = require('@calzoneman/jsli')('playlist');
|
const LOGGER = require('@calzoneman/jsli')('playlist');
|
||||||
|
@ -159,15 +158,22 @@ PlaylistModule.prototype.load = function (data) {
|
||||||
}
|
}
|
||||||
} else if (item.media.type === "gd") {
|
} else if (item.media.type === "gd") {
|
||||||
delete item.media.meta.gpdirect;
|
delete item.media.meta.gpdirect;
|
||||||
} else if (["vm", "jw", "mx"].includes(item.media.type)) {
|
} else if (["vm", "jw", "mx", "im", "gp", "us", "hb"].includes(item.media.type)) {
|
||||||
// JW has been deprecated for a long time
|
// JW has been deprecated for a long time
|
||||||
// VM shut down in December 2017
|
// VM shut down in December 2017
|
||||||
// Mixer shut down in July 2020
|
// Mixer shut down in July 2020
|
||||||
|
// Imgur replaced albums with a feature called galleries in 2019
|
||||||
|
// Picasa shut down in 2016
|
||||||
|
// Ustream was sunset by IBM in September 2018
|
||||||
|
// SmashCast (Hitbox) seemed to just vanish November 2020
|
||||||
LOGGER.warn(
|
LOGGER.warn(
|
||||||
"Dropping playlist item with deprecated type %s",
|
"Dropping playlist item with deprecated type %s",
|
||||||
item.media.type
|
item.media.type
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
} else if (item.media.meta.embed && item.media.meta.embed.tag !== 'iframe') {
|
||||||
|
LOGGER.warn("Dropping playlist item with flash embed");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var m = new Media(item.media.id, item.media.title, item.media.seconds,
|
var m = new Media(item.media.id, item.media.title, item.media.seconds,
|
||||||
|
@ -512,7 +518,6 @@ PlaylistModule.prototype.queueStandard = function (user, data) {
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
this.channel.refCounter.ref("PlaylistModule::queueStandard");
|
this.channel.refCounter.ref("PlaylistModule::queueStandard");
|
||||||
counters.add("playlist:queue:count", 1);
|
|
||||||
this.semaphore.queue(function (lock) {
|
this.semaphore.queue(function (lock) {
|
||||||
InfoGetter.getMedia(data.id, data.type, function (err, media) {
|
InfoGetter.getMedia(data.id, data.type, function (err, media) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ const TYPE_NEW_POLL = {
|
||||||
title: "string",
|
title: "string",
|
||||||
timeout: "number,optional",
|
timeout: "number,optional",
|
||||||
obscured: "boolean",
|
obscured: "boolean",
|
||||||
|
retainVotes: "boolean,optional",
|
||||||
opts: "array"
|
opts: "array"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,12 +43,7 @@ PollModule.prototype.unload = function () {
|
||||||
PollModule.prototype.load = function (data) {
|
PollModule.prototype.load = function (data) {
|
||||||
if ("poll" in data) {
|
if ("poll" in data) {
|
||||||
if (data.poll !== null) {
|
if (data.poll !== null) {
|
||||||
this.poll = new Poll(data.poll.initiator, "", [], data.poll.obscured);
|
this.poll = Poll.fromChannelData(data.poll);
|
||||||
this.poll.title = data.poll.title;
|
|
||||||
this.poll.options = data.poll.options;
|
|
||||||
this.poll.counts = data.poll.counts;
|
|
||||||
this.poll.votes = data.poll.votes;
|
|
||||||
this.poll.timestamp = data.poll.timestamp;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,15 +56,7 @@ PollModule.prototype.save = function (data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.poll = {
|
data.poll = this.poll.toChannelData();
|
||||||
title: this.poll.title,
|
|
||||||
initiator: this.poll.initiator,
|
|
||||||
options: this.poll.options,
|
|
||||||
counts: this.poll.counts,
|
|
||||||
votes: this.poll.votes,
|
|
||||||
obscured: this.poll.obscured,
|
|
||||||
timestamp: this.poll.timestamp
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
PollModule.prototype.onUserPostJoin = function (user) {
|
PollModule.prototype.onUserPostJoin = function (user) {
|
||||||
|
@ -97,8 +85,7 @@ PollModule.prototype.addUserToPollRoom = function (user) {
|
||||||
};
|
};
|
||||||
|
|
||||||
PollModule.prototype.onUserPart = function(user) {
|
PollModule.prototype.onUserPart = function(user) {
|
||||||
if (this.poll) {
|
if (this.poll && !this.poll.retainVotes && this.poll.uncountVote(user.realip)) {
|
||||||
this.poll.unvote(user.realip);
|
|
||||||
this.broadcastPoll(false);
|
this.broadcastPoll(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -110,12 +97,11 @@ PollModule.prototype.sendPoll = function (user) {
|
||||||
|
|
||||||
var perms = this.channel.modules.permissions;
|
var perms = this.channel.modules.permissions;
|
||||||
|
|
||||||
user.socket.emit("closePoll");
|
|
||||||
if (perms.canViewHiddenPoll(user)) {
|
if (perms.canViewHiddenPoll(user)) {
|
||||||
var unobscured = this.poll.packUpdate(true);
|
var unobscured = this.poll.toUpdateFrame(true);
|
||||||
user.socket.emit("newPoll", unobscured);
|
user.socket.emit("newPoll", unobscured);
|
||||||
} else {
|
} else {
|
||||||
var obscured = this.poll.packUpdate(false);
|
var obscured = this.poll.toUpdateFrame(false);
|
||||||
user.socket.emit("newPoll", obscured);
|
user.socket.emit("newPoll", obscured);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -125,13 +111,10 @@ PollModule.prototype.broadcastPoll = function (isNewPoll) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var obscured = this.poll.packUpdate(false);
|
var obscured = this.poll.toUpdateFrame(false);
|
||||||
var unobscured = this.poll.packUpdate(true);
|
var unobscured = this.poll.toUpdateFrame(true);
|
||||||
|
|
||||||
const event = isNewPoll ? "newPoll" : "updatePoll";
|
const event = isNewPoll ? "newPoll" : "updatePoll";
|
||||||
if (isNewPoll) {
|
|
||||||
this.channel.broadcastAll("closePoll");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.channel.broadcastToRoom(event, unobscured, this.roomViewHidden);
|
this.channel.broadcastToRoom(event, unobscured, this.roomViewHidden);
|
||||||
this.channel.broadcastToRoom(event, obscured, this.roomNoViewHidden);
|
this.channel.broadcastToRoom(event, obscured, this.roomNoViewHidden);
|
||||||
|
@ -165,6 +148,9 @@ PollModule.prototype.handleNewPoll = function (user, data, ack) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure any existing poll is closed
|
||||||
|
this.handleClosePoll(user);
|
||||||
|
|
||||||
ack = ackOrErrorMsg(ack, user);
|
ack = ackOrErrorMsg(ack, user);
|
||||||
|
|
||||||
if (typeof data !== 'object' || data === null) {
|
if (typeof data !== 'object' || data === null) {
|
||||||
|
@ -197,7 +183,15 @@ PollModule.prototype.handleNewPoll = function (user, data, ack) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var poll = new Poll(user.getName(), data.title, data.opts, data.obscured);
|
var poll = Poll.create(
|
||||||
|
user.getName(),
|
||||||
|
data.title,
|
||||||
|
data.opts,
|
||||||
|
{
|
||||||
|
hideVotes: data.obscured,
|
||||||
|
retainVotes: data.retainVotes === undefined ? false : data.retainVotes
|
||||||
|
}
|
||||||
|
);
|
||||||
var self = this;
|
var self = this;
|
||||||
if (data.hasOwnProperty("timeout")) {
|
if (data.hasOwnProperty("timeout")) {
|
||||||
poll.timer = setTimeout(function () {
|
poll.timer = setTimeout(function () {
|
||||||
|
@ -223,9 +217,10 @@ PollModule.prototype.handleVote = function (user, data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.poll) {
|
if (this.poll) {
|
||||||
this.poll.vote(user.realip, data.option);
|
if (this.poll.countVote(user.realip, data.option)) {
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
this.broadcastPoll(false);
|
this.broadcastPoll(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -235,9 +230,9 @@ PollModule.prototype.handleClosePoll = function (user) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.poll) {
|
if (this.poll) {
|
||||||
if (this.poll.obscured) {
|
if (this.poll.hideVotes) {
|
||||||
this.poll.obscured = false;
|
this.poll.hideVotes = false;
|
||||||
this.channel.broadcastAll("updatePoll", this.poll.packUpdate(true));
|
this.channel.broadcastAll("updatePoll", this.poll.toUpdateFrame(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.poll.timer) {
|
if (this.poll.timer) {
|
||||||
|
@ -256,6 +251,9 @@ PollModule.prototype.handlePollCmd = function (obscured, user, msg, _meta) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure any existing poll is closed
|
||||||
|
this.handleClosePoll(user);
|
||||||
|
|
||||||
msg = msg.replace(/^\/h?poll/, "");
|
msg = msg.replace(/^\/h?poll/, "");
|
||||||
|
|
||||||
var args = msg.split(",");
|
var args = msg.split(",");
|
||||||
|
@ -270,7 +268,7 @@ PollModule.prototype.handlePollCmd = function (obscured, user, msg, _meta) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var poll = new Poll(user.getName(), title, args, obscured);
|
var poll = Poll.create(user.getName(), title, args, { hideVotes: obscured });
|
||||||
this.poll = poll;
|
this.poll = poll;
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
this.broadcastPoll(true);
|
this.broadcastPoll(true);
|
||||||
|
|
|
@ -37,10 +37,10 @@ VoteskipModule.prototype.handleVoteskip = function (user) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.poll) {
|
if (!this.poll) {
|
||||||
this.poll = new Poll("[server]", "voteskip", ["skip"], false);
|
this.poll = Poll.create("[server]", "voteskip", ["skip"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.poll.vote(user.realip, 0)) {
|
if (!this.poll.countVote(user.realip, 0)) {
|
||||||
// Vote was already recorded for this IP, no update needed
|
// Vote was already recorded for this IP, no update needed
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ VoteskipModule.prototype.unvote = function(ip) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.poll.unvote(ip);
|
this.poll.uncountVote(ip);
|
||||||
};
|
};
|
||||||
|
|
||||||
VoteskipModule.prototype.update = function () {
|
VoteskipModule.prototype.update = function () {
|
||||||
|
@ -78,10 +78,14 @@ VoteskipModule.prototype.update = function () {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { counts } = this.poll.toUpdateFrame(false);
|
||||||
const { total, eligible, noPermission, afk } = this.calcUsercounts();
|
const { total, eligible, noPermission, afk } = this.calcUsercounts();
|
||||||
const need = Math.ceil(eligible * this.channel.modules.options.get("voteskip_ratio"));
|
const need = Math.max(
|
||||||
if (this.poll.counts[0] >= need) {
|
1, // Require at least one vote, see #944
|
||||||
const info = `${this.poll.counts[0]}/${eligible} skipped; ` +
|
Math.ceil(eligible * this.channel.modules.options.get("voteskip_ratio"))
|
||||||
|
);
|
||||||
|
if (counts[0] >= need) {
|
||||||
|
const info = `${counts[0]}/${eligible} skipped; ` +
|
||||||
`eligible voters: ${eligible} = total (${total}) - AFK (${afk}) ` +
|
`eligible voters: ${eligible} = total (${total}) - AFK (${afk}) ` +
|
||||||
`- no permission (${noPermission}); ` +
|
`- no permission (${noPermission}); ` +
|
||||||
`ratio = ${this.channel.modules.options.get("voteskip_ratio")}`;
|
`ratio = ${this.channel.modules.options.get("voteskip_ratio")}`;
|
||||||
|
@ -107,11 +111,20 @@ VoteskipModule.prototype.update = function () {
|
||||||
|
|
||||||
VoteskipModule.prototype.sendVoteskipData = function (users) {
|
VoteskipModule.prototype.sendVoteskipData = function (users) {
|
||||||
const { eligible } = this.calcUsercounts();
|
const { eligible } = this.calcUsercounts();
|
||||||
var data = {
|
let data;
|
||||||
count: this.poll ? this.poll.counts[0] : 0,
|
|
||||||
need: this.poll ? Math.ceil(eligible * this.channel.modules.options.get("voteskip_ratio"))
|
if (this.poll) {
|
||||||
: 0
|
const { counts } = this.poll.toUpdateFrame(false);
|
||||||
};
|
data = {
|
||||||
|
count: counts[0],
|
||||||
|
need: Math.ceil(eligible * this.channel.modules.options.get("voteskip_ratio"))
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data = {
|
||||||
|
count: 0,
|
||||||
|
need: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var perms = this.channel.modules.permissions;
|
var perms = this.channel.modules.permissions;
|
||||||
|
|
||||||
|
|
24
src/cli/banned-channels.js
Normal file
24
src/cli/banned-channels.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import Server from '../server';
|
||||||
|
|
||||||
|
export async function handleBanChannel({ name, externalReason, internalReason }) {
|
||||||
|
await Server.getServer().bannedChannelsController.banChannel({
|
||||||
|
name,
|
||||||
|
externalReason,
|
||||||
|
internalReason,
|
||||||
|
bannedBy: '[console]'
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status: 'success' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUnbanChannel({ name }) {
|
||||||
|
await Server.getServer().bannedChannelsController.unbanChannel(name, '[console]');
|
||||||
|
|
||||||
|
return { status: 'success' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleShowBannedChannel({ name }) {
|
||||||
|
let banInfo = await Server.getServer().bannedChannelsController.getBannedChannel(name);
|
||||||
|
|
||||||
|
return { status: 'success', ban: banInfo };
|
||||||
|
}
|
|
@ -11,7 +11,8 @@ import { CaptchaConfig } from './configuration/captchaconfig';
|
||||||
const LOGGER = require('@calzoneman/jsli')('config');
|
const LOGGER = require('@calzoneman/jsli')('config');
|
||||||
|
|
||||||
var defaults = {
|
var defaults = {
|
||||||
mysql: {
|
database: {
|
||||||
|
client: "mysql",
|
||||||
server: "localhost",
|
server: "localhost",
|
||||||
port: 3306,
|
port: 3306,
|
||||||
database: "cytube3",
|
database: "cytube3",
|
||||||
|
@ -60,15 +61,18 @@ var defaults = {
|
||||||
io: {
|
io: {
|
||||||
domain: "http://localhost",
|
domain: "http://localhost",
|
||||||
"default-port": 1337,
|
"default-port": 1337,
|
||||||
"ip-connection-limit": 10
|
"ip-connection-limit": 10,
|
||||||
|
cors: {
|
||||||
|
"allowed-origins": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"youtube-v3-key": "",
|
"youtube-v3-key": "",
|
||||||
"channel-blacklist": [],
|
|
||||||
"channel-path": "r",
|
"channel-path": "r",
|
||||||
"channel-save-interval": 5,
|
"channel-save-interval": 5,
|
||||||
"max-channels-per-user": 5,
|
"max-channels-per-user": 5,
|
||||||
"max-accounts-per-ip": 5,
|
"max-accounts-per-ip": 5,
|
||||||
"guest-login-delay": 60,
|
"guest-login-delay": 60,
|
||||||
|
"max-chat-message-length": 320,
|
||||||
aliases: {
|
aliases: {
|
||||||
"purge-interval": 3600000,
|
"purge-interval": 3600000,
|
||||||
"max-age": 2592000000
|
"max-age": 2592000000
|
||||||
|
@ -369,13 +373,6 @@ function preprocessConfig(cfg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Convert channel blacklist to a hashtable */
|
|
||||||
var tbl = {};
|
|
||||||
cfg["channel-blacklist"].forEach(function (c) {
|
|
||||||
tbl[c.toLowerCase()] = true;
|
|
||||||
});
|
|
||||||
cfg["channel-blacklist"] = tbl;
|
|
||||||
|
|
||||||
/* Check channel path */
|
/* Check channel path */
|
||||||
if(!/^[-\w]+$/.test(cfg["channel-path"])){
|
if(!/^[-\w]+$/.test(cfg["channel-path"])){
|
||||||
LOGGER.error("Channel paths may only use the same characters as usernames and channel names.");
|
LOGGER.error("Channel paths may only use the same characters as usernames and channel names.");
|
||||||
|
@ -432,6 +429,11 @@ function preprocessConfig(cfg) {
|
||||||
cfg['channel-storage'] = { type: undefined };
|
cfg['channel-storage'] = { type: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cfg["max-chat-message-length"] > 1000) {
|
||||||
|
LOGGER.warn("Max chat message length was greater than 1000. Setting to 1000.");
|
||||||
|
cfg["max-chat-message-length"] = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
67
src/controller/banned-channels.js
Normal file
67
src/controller/banned-channels.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { eventlog } from '../logger';
|
||||||
|
import { SimpleCache } from '../util/simple-cache';
|
||||||
|
const LOGGER = require('@calzoneman/jsli')('BannedChannelsController');
|
||||||
|
|
||||||
|
export class BannedChannelsController {
|
||||||
|
constructor(dbChannels, globalMessageBus) {
|
||||||
|
this.dbChannels = dbChannels;
|
||||||
|
this.globalMessageBus = globalMessageBus;
|
||||||
|
this.cache = new SimpleCache({
|
||||||
|
maxElem: 1000,
|
||||||
|
maxAge: 5 * 60_000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: add an audit log to the database
|
||||||
|
*/
|
||||||
|
|
||||||
|
async banChannel({ name, externalReason, internalReason, bannedBy }) {
|
||||||
|
LOGGER.info(`Banning channel ${name} (banned by ${bannedBy})`);
|
||||||
|
eventlog.log(`[acp] ${bannedBy} banned channel ${name}`);
|
||||||
|
|
||||||
|
let banInfo = await this.dbChannels.getBannedChannel(name);
|
||||||
|
if (banInfo !== null) {
|
||||||
|
LOGGER.warn(`Channel ${name} is already banned, updating ban reason`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cache.delete(name);
|
||||||
|
|
||||||
|
await this.dbChannels.putBannedChannel({
|
||||||
|
name,
|
||||||
|
externalReason,
|
||||||
|
internalReason,
|
||||||
|
bannedBy
|
||||||
|
});
|
||||||
|
|
||||||
|
this.globalMessageBus.emit(
|
||||||
|
'ChannelBanned',
|
||||||
|
{ channel: name, externalReason }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unbanChannel(name, unbannedBy) {
|
||||||
|
LOGGER.info(`Unbanning channel ${name}`);
|
||||||
|
eventlog.log(`[acp] ${unbannedBy} unbanned channel ${name}`);
|
||||||
|
this.cache.delete(name);
|
||||||
|
|
||||||
|
this.globalMessageBus.emit(
|
||||||
|
'ChannelUnbanned',
|
||||||
|
{ channel: name }
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.dbChannels.removeBannedChannel(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBannedChannel(name) {
|
||||||
|
name = name.toLowerCase();
|
||||||
|
|
||||||
|
let info = this.cache.get(name);
|
||||||
|
if (info === null) {
|
||||||
|
info = await this.dbChannels.getBannedChannel(name);
|
||||||
|
this.cache.put(name, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
import io from 'socket.io';
|
|
||||||
import Socket from 'socket.io/lib/socket';
|
|
||||||
import * as Metrics from './metrics/metrics';
|
|
||||||
import { JSONFileMetricsReporter } from './metrics/jsonfilemetricsreporter';
|
|
||||||
|
|
||||||
const LOGGER = require('@calzoneman/jsli')('counters');
|
|
||||||
|
|
||||||
var server = null;
|
|
||||||
|
|
||||||
exports.add = Metrics.incCounter;
|
|
||||||
|
|
||||||
Socket.prototype._packet = Socket.prototype.packet;
|
|
||||||
Socket.prototype.packet = function () {
|
|
||||||
this._packet.apply(this, arguments);
|
|
||||||
exports.add('socket.io:packet');
|
|
||||||
};
|
|
||||||
|
|
||||||
function getConnectedSockets() {
|
|
||||||
var sockets = io.instance.sockets.sockets;
|
|
||||||
if (typeof sockets.length === 'number') {
|
|
||||||
return sockets.length;
|
|
||||||
} else {
|
|
||||||
return Object.keys(sockets).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setChannelCounts(metrics) {
|
|
||||||
if (server === null) {
|
|
||||||
server = require('./server').getServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var publicCount = 0;
|
|
||||||
var allCount = 0;
|
|
||||||
server.channels.forEach(function (c) {
|
|
||||||
allCount++;
|
|
||||||
if (c.modules.options && c.modules.options.get("show_public")) {
|
|
||||||
publicCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
metrics.addProperty('channelCount:all', allCount);
|
|
||||||
metrics.addProperty('channelCount:public', publicCount);
|
|
||||||
} catch (error) {
|
|
||||||
LOGGER.error(error.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reporter = new JSONFileMetricsReporter('counters.log');
|
|
||||||
Metrics.setReporter(reporter);
|
|
||||||
Metrics.setReportInterval(60000);
|
|
||||||
Metrics.addReportHook((metrics) => {
|
|
||||||
metrics.addProperty('socket.io:count', getConnectedSockets());
|
|
||||||
setChannelCounts(metrics);
|
|
||||||
});
|
|
|
@ -22,15 +22,21 @@ const SOURCE_CONTENT_TYPES = new Set([
|
||||||
'application/dash+xml',
|
'application/dash+xml',
|
||||||
'application/x-mpegURL',
|
'application/x-mpegURL',
|
||||||
'audio/aac',
|
'audio/aac',
|
||||||
'audio/ogg',
|
'audio/mp4',
|
||||||
'audio/mpeg',
|
'audio/mpeg',
|
||||||
|
'audio/ogg',
|
||||||
|
'audio/opus',
|
||||||
'video/mp4',
|
'video/mp4',
|
||||||
'video/ogg',
|
'video/ogg',
|
||||||
'video/webm'
|
'video/webm'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const LIVE_ONLY_CONTENT_TYPES = new Set([
|
const AUDIO_ONLY_CONTENT_TYPES = new Set([
|
||||||
'application/dash+xml'
|
'audio/aac',
|
||||||
|
'audio/mp4',
|
||||||
|
'audio/mpeg',
|
||||||
|
'audio/ogg',
|
||||||
|
'audio/opus'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function lookup(url, opts) {
|
export function lookup(url, opts) {
|
||||||
|
@ -134,6 +140,7 @@ export function convert(id, data) {
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
direct: sources,
|
direct: sources,
|
||||||
|
audioTracks: data.audioTracks,
|
||||||
textTracks: data.textTracks,
|
textTracks: data.textTracks,
|
||||||
thumbnail: data.thumbnail, // Currently ignored by Media
|
thumbnail: data.thumbnail, // Currently ignored by Media
|
||||||
live: !!data.live // Currently ignored by Media
|
live: !!data.live // Currently ignored by Media
|
||||||
|
@ -162,11 +169,20 @@ export function validate(data) {
|
||||||
validateURL(data.thumbnail);
|
validateURL(data.thumbnail);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateSources(data.sources, data);
|
validateSources(data.sources);
|
||||||
|
validateAudioTracks(data.audioTracks);
|
||||||
validateTextTracks(data.textTracks);
|
validateTextTracks(data.textTracks);
|
||||||
|
/*
|
||||||
|
* TODO: Xaekai's Octopus subtitle support uses a separate subTracks array
|
||||||
|
* in a slightly different format than textTracks. That currently requires
|
||||||
|
* a channel script to use, but if that is integrated in core then it needs
|
||||||
|
* to be validated here (and ideally merged with textTracks so there is only
|
||||||
|
* one array).
|
||||||
|
*/
|
||||||
|
validateFonts(data.fonts);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateSources(sources, data) {
|
function validateSources(sources) {
|
||||||
if (!Array.isArray(sources))
|
if (!Array.isArray(sources))
|
||||||
throw new ValidationError('sources must be a list');
|
throw new ValidationError('sources must be a list');
|
||||||
if (sources.length === 0)
|
if (sources.length === 0)
|
||||||
|
@ -182,12 +198,8 @@ function validateSources(sources, data) {
|
||||||
`unacceptable source contentType "${source.contentType}"`
|
`unacceptable source contentType "${source.contentType}"`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (LIVE_ONLY_CONTENT_TYPES.has(source.contentType) && !data.live)
|
// TODO (Xaekai): This should be allowed
|
||||||
throw new ValidationError(
|
if (/*!AUDIO_ONLY_CONTENT_TYPES.has(source.contentType) && */!SOURCE_QUALITIES.has(source.quality))
|
||||||
`contentType "${source.contentType}" requires live: true`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!SOURCE_QUALITIES.has(source.quality))
|
|
||||||
throw new ValidationError(`unacceptable source quality "${source.quality}"`);
|
throw new ValidationError(`unacceptable source quality "${source.quality}"`);
|
||||||
|
|
||||||
if (source.hasOwnProperty('bitrate')) {
|
if (source.hasOwnProperty('bitrate')) {
|
||||||
|
@ -201,6 +213,45 @@ function validateSources(sources, data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateAudioTracks(audioTracks) {
|
||||||
|
if (typeof audioTracks === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(audioTracks)){
|
||||||
|
throw new ValidationError('audioTracks must be a list');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let track of audioTracks) {
|
||||||
|
if (typeof track.url !== 'string'){
|
||||||
|
throw new ValidationError('audio track URL must be a string');
|
||||||
|
}
|
||||||
|
validateURL(track.url);
|
||||||
|
|
||||||
|
if (!AUDIO_ONLY_CONTENT_TYPES.has(track.contentType)){
|
||||||
|
throw new ValidationError(
|
||||||
|
`unacceptable audio track contentType "${track.contentType}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof track.label !== 'string'){
|
||||||
|
throw new ValidationError('audio track label must be a string');
|
||||||
|
}
|
||||||
|
if (!track.label){
|
||||||
|
throw new ValidationError('audio track label must be nonempty');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof track.language !== 'string'){
|
||||||
|
throw new ValidationError('audio track language must be a string');
|
||||||
|
}
|
||||||
|
if (!track.language){
|
||||||
|
throw new ValidationError('audio track language must be nonempty');
|
||||||
|
}
|
||||||
|
if (!/^[a-z]{2,3}$/.test(track.language)){
|
||||||
|
throw new ValidationError('audio track language must be a two or three letter IETF BCP 47 subtag');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function validateTextTracks(textTracks) {
|
function validateTextTracks(textTracks) {
|
||||||
if (typeof textTracks === 'undefined') {
|
if (typeof textTracks === 'undefined') {
|
||||||
return;
|
return;
|
||||||
|
@ -236,6 +287,20 @@ function validateTextTracks(textTracks) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateFonts(fonts) {
|
||||||
|
if (typeof textTracks === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(fonts))
|
||||||
|
throw new ValidationError('fonts must be a list of URLs');
|
||||||
|
|
||||||
|
for (let f of fonts) {
|
||||||
|
if (typeof f !== 'string')
|
||||||
|
throw new ValidationError('fonts must be a list of URLs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseURL(urlstring) {
|
function parseURL(urlstring) {
|
||||||
const url = urlParse(urlstring);
|
const url = urlParse(urlstring);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
var Config = require("./config");
|
var Config = require("./config");
|
||||||
var tables = require("./database/tables");
|
var tables = require("./database/tables");
|
||||||
import * as Metrics from './metrics/metrics';
|
|
||||||
import knex from 'knex';
|
import knex from 'knex';
|
||||||
import { GlobalBanDB } from './db/globalban';
|
import { GlobalBanDB } from './db/globalban';
|
||||||
import { MetadataCacheDB } from './database/metadata_cache';
|
import { MetadataCacheDB } from './database/metadata_cache';
|
||||||
|
@ -31,19 +30,19 @@ class Database {
|
||||||
constructor(knexConfig = null) {
|
constructor(knexConfig = null) {
|
||||||
if (knexConfig === null) {
|
if (knexConfig === null) {
|
||||||
knexConfig = {
|
knexConfig = {
|
||||||
client: 'mysql',
|
client: Config.get('database.client'),
|
||||||
connection: {
|
connection: {
|
||||||
host: Config.get('mysql.server'),
|
host: Config.get('database.server'),
|
||||||
port: Config.get('mysql.port'),
|
port: Config.get('database.port'),
|
||||||
user: Config.get('mysql.user'),
|
user: Config.get('database.user'),
|
||||||
password: Config.get('mysql.password'),
|
password: Config.get('database.password'),
|
||||||
database: Config.get('mysql.database'),
|
database: Config.get('database.database'),
|
||||||
multipleStatements: true, // Legacy thing
|
multipleStatements: true, // Legacy thing
|
||||||
charset: 'utf8mb4'
|
charset: 'utf8mb4'
|
||||||
},
|
},
|
||||||
pool: {
|
pool: {
|
||||||
min: Config.get('mysql.pool-size'),
|
min: Config.get('database.pool-size'),
|
||||||
max: Config.get('mysql.pool-size')
|
max: Config.get('database.pool-size')
|
||||||
},
|
},
|
||||||
debug: !!process.env.KNEX_DEBUG
|
debug: !!process.env.KNEX_DEBUG
|
||||||
};
|
};
|
||||||
|
@ -53,14 +52,12 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
runTransaction(fn) {
|
runTransaction(fn) {
|
||||||
const timer = Metrics.startTimer('db:queryTime');
|
|
||||||
const end = queryLatency.startTimer();
|
const end = queryLatency.startTimer();
|
||||||
return this.knex.transaction(fn).catch(error => {
|
return this.knex.transaction(fn).catch(error => {
|
||||||
queryErrorCount.inc(1);
|
queryErrorCount.inc(1);
|
||||||
throw error;
|
throw error;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
end();
|
end();
|
||||||
Metrics.stopTimer(timer);
|
|
||||||
queryCount.inc(1);
|
queryCount.inc(1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -76,6 +73,8 @@ module.exports.init = function (newDB) {
|
||||||
} else {
|
} else {
|
||||||
db = new Database();
|
db = new Database();
|
||||||
}
|
}
|
||||||
|
// FIXME Initial database connection failed: error: select 1 from dual
|
||||||
|
// relation "dual" does not exist
|
||||||
db.knex.raw('select 1 from dual')
|
db.knex.raw('select 1 from dual')
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
LOGGER.error('Initial database connection failed: %s', error.stack);
|
LOGGER.error('Initial database connection failed: %s', error.stack);
|
||||||
|
@ -88,6 +87,9 @@ module.exports.init = function (newDB) {
|
||||||
require('@cytube/mediaquery/lib/provider/youtube').setCache(
|
require('@cytube/mediaquery/lib/provider/youtube').setCache(
|
||||||
new MetadataCacheDB(db)
|
new MetadataCacheDB(db)
|
||||||
);
|
);
|
||||||
|
require('@cytube/mediaquery/lib/provider/bitchute').setCache(
|
||||||
|
new MetadataCacheDB(db)
|
||||||
|
);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
LOGGER.error(error.stack);
|
LOGGER.error(error.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@ -110,7 +112,6 @@ module.exports.getGlobalBanDB = function getGlobalBanDB() {
|
||||||
* Execute a database query
|
* Execute a database query
|
||||||
*/
|
*/
|
||||||
module.exports.query = function (query, sub, callback) {
|
module.exports.query = function (query, sub, callback) {
|
||||||
const timer = Metrics.startTimer('db:queryTime');
|
|
||||||
// 2nd argument is optional
|
// 2nd argument is optional
|
||||||
if (typeof sub === "function") {
|
if (typeof sub === "function") {
|
||||||
callback = sub;
|
callback = sub;
|
||||||
|
@ -157,7 +158,6 @@ module.exports.query = function (query, sub, callback) {
|
||||||
process.nextTick(callback, 'Database failure', null);
|
process.nextTick(callback, 'Database failure', null);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
end();
|
end();
|
||||||
Metrics.stopTimer(timer);
|
|
||||||
queryCount.inc(1);
|
queryCount.inc(1);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ var db = require("../database");
|
||||||
var valid = require("../utilities").isValidChannelName;
|
var valid = require("../utilities").isValidChannelName;
|
||||||
var Flags = require("../flags");
|
var Flags = require("../flags");
|
||||||
var util = require("../utilities");
|
var util = require("../utilities");
|
||||||
|
// TODO: I think newer knex has native support for this
|
||||||
import { createMySQLDuplicateKeyUpdate } from '../util/on-duplicate-key-update';
|
import { createMySQLDuplicateKeyUpdate } from '../util/on-duplicate-key-update';
|
||||||
import Config from '../config';
|
import Config from '../config';
|
||||||
|
|
||||||
|
@ -209,7 +210,9 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query("SELECT * FROM `channels` WHERE owner=?", [owner],
|
db.query("SELECT c.*, bc.external_reason as banReason " +
|
||||||
|
"FROM channels c LEFT OUTER JOIN banned_channels bc " +
|
||||||
|
"ON bc.channel_name = c.name WHERE c.owner=?", [owner],
|
||||||
function (err, res) {
|
function (err, res) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err, []);
|
callback(err, []);
|
||||||
|
@ -245,13 +248,28 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query("SELECT * FROM `channels` WHERE name=?", chan.name, function (err, res) {
|
db.query("SELECT c.*, bc.external_reason as banReason " +
|
||||||
|
"FROM channels c LEFT OUTER JOIN banned_channels bc " +
|
||||||
|
"ON bc.channel_name = c.name WHERE c.name=? " +
|
||||||
|
"UNION " +
|
||||||
|
"SELECT c.*, bc.external_reason as banReason " +
|
||||||
|
"FROM channels c RIGHT OUTER JOIN banned_channels bc " +
|
||||||
|
"ON bc.channel_name = c.name WHERE bc.channel_name=? ",
|
||||||
|
[chan.name, chan.name],
|
||||||
|
function (err, res) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err, null);
|
callback(err, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.length === 0) {
|
if (res.length > 0 && res[0].banReason !== null) {
|
||||||
|
let banError = new Error(`Channel is banned: ${res[0].banReason}`);
|
||||||
|
banError.code = 'EBANNED';
|
||||||
|
callback(banError, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.length === 0 || res[0].id === null) {
|
||||||
callback("Channel is not registered", null);
|
callback("Channel is not registered", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -704,5 +722,63 @@ module.exports = {
|
||||||
LOGGER.error(`Failed to update owner_last_seen column for channel ID ${channelId}: ${error}`);
|
LOGGER.error(`Failed to update owner_last_seen column for channel ID ${channelId}: ${error}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getBannedChannel: async function getBannedChannel(name) {
|
||||||
|
if (!valid(name)) {
|
||||||
|
throw new Error("Invalid channel name");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await db.getDB().runTransaction(async tx => {
|
||||||
|
let rows = await tx.table('banned_channels')
|
||||||
|
.where({ channel_name: name })
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
channelName: rows[0].channel_name,
|
||||||
|
externalReason: rows[0].external_reason,
|
||||||
|
internalReason: rows[0].internal_reason,
|
||||||
|
bannedBy: rows[0].banned_by,
|
||||||
|
createdAt: rows[0].created_at,
|
||||||
|
updatedAt: rows[0].updated_at
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
putBannedChannel: async function putBannedChannel({ name, externalReason, internalReason, bannedBy }) {
|
||||||
|
if (!valid(name)) {
|
||||||
|
throw new Error("Invalid channel name");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await db.getDB().runTransaction(async tx => {
|
||||||
|
let insert = tx.table('banned_channels')
|
||||||
|
.insert({
|
||||||
|
channel_name: name,
|
||||||
|
external_reason: externalReason,
|
||||||
|
internal_reason: internalReason,
|
||||||
|
banned_by: bannedBy
|
||||||
|
});
|
||||||
|
let update = tx.raw(createMySQLDuplicateKeyUpdate(
|
||||||
|
['external_reason', 'internal_reason', 'banned_by']
|
||||||
|
));
|
||||||
|
|
||||||
|
return tx.raw(insert.toString() + update.toString());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeBannedChannel: async function removeBannedChannel(name) {
|
||||||
|
if (!valid(name)) {
|
||||||
|
throw new Error("Invalid channel name");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await db.getDB().runTransaction(async tx => {
|
||||||
|
await tx.table('banned_channels')
|
||||||
|
.where({ channel_name: name })
|
||||||
|
.delete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,8 @@ function mediaquery2cytube(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'youtube':
|
case 'youtube':
|
||||||
return 'yt';
|
return 'yt';
|
||||||
|
case 'bitchute':
|
||||||
|
return 'bc';
|
||||||
default:
|
default:
|
||||||
throw new Error(`mediaquery2cytube: no mapping for ${type}`);
|
throw new Error(`mediaquery2cytube: no mapping for ${type}`);
|
||||||
}
|
}
|
||||||
|
@ -18,14 +20,17 @@ function cytube2mediaquery(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'yt':
|
case 'yt':
|
||||||
return 'youtube';
|
return 'youtube';
|
||||||
|
case 'bc':
|
||||||
|
return 'bitchute';
|
||||||
default:
|
default:
|
||||||
throw new Error(`cytube2mediaquery: no mapping for ${type}`);
|
throw new Error(`cytube2mediaquery: no mapping for ${type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedResultAge = new Summary({
|
const cachedResultAge = new Summary({
|
||||||
name: 'cytube_yt_cache_result_age_seconds',
|
name: 'cytube_media_cache_result_age_seconds',
|
||||||
help: 'Age (in seconds) of cached record'
|
help: 'Age (in seconds) of cached record',
|
||||||
|
labelNames: ['source']
|
||||||
});
|
});
|
||||||
|
|
||||||
class MetadataCacheDB {
|
class MetadataCacheDB {
|
||||||
|
@ -62,10 +67,11 @@ class MetadataCacheDB {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let age = 0;
|
||||||
try {
|
try {
|
||||||
let age = (Date.now() - row.updated_at.getTime())/1000;
|
age = (Date.now() - row.updated_at.getTime())/1000;
|
||||||
if (age > 0) {
|
if (age > 0) {
|
||||||
cachedResultAge.observe(age);
|
cachedResultAge.labels(type).observe(age);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LOGGER.error('Failed to record cachedResultAge metric: %s', error.stack);
|
LOGGER.error('Failed to record cachedResultAge metric: %s', error.stack);
|
||||||
|
@ -73,6 +79,7 @@ class MetadataCacheDB {
|
||||||
|
|
||||||
let metadata = JSON.parse(row.metadata);
|
let metadata = JSON.parse(row.metadata);
|
||||||
metadata.type = cytube2mediaquery(metadata.type);
|
metadata.type = cytube2mediaquery(metadata.type);
|
||||||
|
metadata.meta.cacheAge = age;
|
||||||
return new Media(metadata);
|
return new Media(metadata);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,4 +156,15 @@ export async function initTables() {
|
||||||
t.primary(['type', 'id']);
|
t.primary(['type', 'id']);
|
||||||
t.index('updated_at');
|
t.index('updated_at');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await ensureTable('banned_channels', t => {
|
||||||
|
t.charset('utf8mb4');
|
||||||
|
t.string('channel_name', 30)
|
||||||
|
.notNullable()
|
||||||
|
.unique();
|
||||||
|
t.text('external_reason').notNullable();
|
||||||
|
t.text('internal_reason').notNullable();
|
||||||
|
t.string('banned_by', 20).notNullable();
|
||||||
|
t.timestamps(/* useTimestamps */ true, /* defaultToNow */ true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,13 +80,16 @@ var acceptedCodecs = {
|
||||||
"flv/h264": true,
|
"flv/h264": true,
|
||||||
"matroska/vp8": true,
|
"matroska/vp8": true,
|
||||||
"matroska/vp9": true,
|
"matroska/vp9": true,
|
||||||
"ogg/theora": true
|
"ogg/theora": true,
|
||||||
|
"mov/av1": true,
|
||||||
|
"matroska/av1": true
|
||||||
};
|
};
|
||||||
|
|
||||||
var acceptedAudioCodecs = {
|
var acceptedAudioCodecs = {
|
||||||
"mp3": true,
|
"mp3": true,
|
||||||
"vorbis": true,
|
"vorbis": true,
|
||||||
"aac": true
|
"aac": true,
|
||||||
|
"opus": true
|
||||||
};
|
};
|
||||||
|
|
||||||
var audioOnlyContainers = {
|
var audioOnlyContainers = {
|
||||||
|
|
247
src/get-info.js
247
src/get-info.js
|
@ -6,6 +6,11 @@ const ffmpeg = require("./ffmpeg");
|
||||||
const mediaquery = require("@cytube/mediaquery");
|
const mediaquery = require("@cytube/mediaquery");
|
||||||
const YouTube = require("@cytube/mediaquery/lib/provider/youtube");
|
const YouTube = require("@cytube/mediaquery/lib/provider/youtube");
|
||||||
const Vimeo = require("@cytube/mediaquery/lib/provider/vimeo");
|
const Vimeo = require("@cytube/mediaquery/lib/provider/vimeo");
|
||||||
|
const Odysee = require("@cytube/mediaquery/lib/provider/odysee");
|
||||||
|
const PeerTube = require("@cytube/mediaquery/lib/provider/peertube");
|
||||||
|
const BitChute = require("@cytube/mediaquery/lib/provider/bitchute");
|
||||||
|
const BandCamp = require("@cytube/mediaquery/lib/provider/bandcamp");
|
||||||
|
const Nicovideo = require("@cytube/mediaquery/lib/provider/nicovideo");
|
||||||
const Streamable = require("@cytube/mediaquery/lib/provider/streamable");
|
const Streamable = require("@cytube/mediaquery/lib/provider/streamable");
|
||||||
const TwitchVOD = require("@cytube/mediaquery/lib/provider/twitch-vod");
|
const TwitchVOD = require("@cytube/mediaquery/lib/provider/twitch-vod");
|
||||||
const TwitchClip = require("@cytube/mediaquery/lib/provider/twitch-clip");
|
const TwitchClip = require("@cytube/mediaquery/lib/provider/twitch-clip");
|
||||||
|
@ -204,114 +209,22 @@ var Getters = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/* soundcloud.com */
|
/* soundcloud.com - see https://github.com/calzoneman/sync/issues/916 */
|
||||||
sc: function (id, callback) {
|
sc: function (id, callback) {
|
||||||
/* TODO: require server owners to register their own API key, put in config */
|
callback(
|
||||||
const SC_CLIENT = "2e0c82ab5a020f3a7509318146128abd";
|
"Soundcloud is not supported anymore due to requiring OAuth but not " +
|
||||||
|
"accepting new API key registrations."
|
||||||
var m = id.match(/([\w-/.:]+)/);
|
);
|
||||||
if (m) {
|
|
||||||
id = m[1];
|
|
||||||
} else {
|
|
||||||
callback("Invalid ID", null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
switch (status) {
|
|
||||||
case 200:
|
|
||||||
case 302:
|
|
||||||
break; /* Request is OK, skip to handling data */
|
|
||||||
case 400:
|
|
||||||
return callback("Invalid request", null);
|
|
||||||
case 403:
|
|
||||||
return callback("Private sound", null);
|
|
||||||
case 404:
|
|
||||||
return callback("Sound not found", null);
|
|
||||||
case 500:
|
|
||||||
case 503:
|
|
||||||
return callback("Service unavailable", null);
|
|
||||||
default:
|
|
||||||
return callback("HTTP " + status, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var track = null;
|
|
||||||
try {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
track = data.location;
|
|
||||||
} catch(e) {
|
|
||||||
callback(e, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options2 = {
|
|
||||||
host: "api.soundcloud.com",
|
|
||||||
port: 443,
|
|
||||||
path: track,
|
|
||||||
method: "GET",
|
|
||||||
dataType: "jsonp",
|
|
||||||
timeout: 1000
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* There has got to be a way to directly get the data I want without
|
|
||||||
* making two requests to Soundcloud...right?
|
|
||||||
* ...right?
|
|
||||||
*/
|
|
||||||
urlRetrieve(https, options2, function (status, data) {
|
|
||||||
switch (status) {
|
|
||||||
case 200:
|
|
||||||
break; /* Request is OK, skip to handling data */
|
|
||||||
case 400:
|
|
||||||
return callback("Invalid request", null);
|
|
||||||
case 403:
|
|
||||||
return callback("Private sound", null);
|
|
||||||
case 404:
|
|
||||||
return callback("Sound not found", null);
|
|
||||||
case 500:
|
|
||||||
case 503:
|
|
||||||
return callback("Service unavailable", null);
|
|
||||||
default:
|
|
||||||
return callback("HTTP " + status, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
var seconds = data.duration / 1000;
|
|
||||||
var title = data.title;
|
|
||||||
var meta = {};
|
|
||||||
if (data.sharing === "private" && data.embeddable_by === "all") {
|
|
||||||
meta.scuri = data.uri;
|
|
||||||
}
|
|
||||||
var media = new Media(id, title, seconds, "sc", meta);
|
|
||||||
callback(false, media);
|
|
||||||
} catch(e) {
|
|
||||||
callback(e, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* livestream.com */
|
/* livestream.com */
|
||||||
li: function (id, callback) {
|
li: function (id, callback) {
|
||||||
var m = id.match(/([\w-]+)/);
|
if (!id.match(/^\d+;\d+$/)) {
|
||||||
if (m) {
|
|
||||||
id = m[1];
|
|
||||||
} else {
|
|
||||||
callback("Invalid ID", null);
|
callback("Invalid ID", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var title = "Livestream.com - " + id;
|
|
||||||
|
var title = "Livestream.com";
|
||||||
var media = new Media(id, title, "--:--", "li");
|
var media = new Media(id, title, "--:--", "li");
|
||||||
callback(false, media);
|
callback(false, media);
|
||||||
},
|
},
|
||||||
|
@ -368,47 +281,6 @@ var Getters = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ustream.tv */
|
|
||||||
us: function (id, callback) {
|
|
||||||
var m = id.match(/(channel\/[^?&#]+)/);
|
|
||||||
if (m) {
|
|
||||||
id = m[1];
|
|
||||||
} else {
|
|
||||||
callback("Invalid ID", null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = {
|
|
||||||
host: "www.ustream.tv",
|
|
||||||
port: 443,
|
|
||||||
path: "/" + id,
|
|
||||||
method: "GET",
|
|
||||||
timeout: 1000
|
|
||||||
};
|
|
||||||
|
|
||||||
urlRetrieve(https, options, function (status, data) {
|
|
||||||
if(status !== 200) {
|
|
||||||
callback("Ustream HTTP " + status, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Yes, regexing this information out of the HTML sucks.
|
|
||||||
* No, there is not a better solution -- it seems IBM
|
|
||||||
* deprecated the old API (or at least replaced with an
|
|
||||||
* enterprise API marked "Contact sales") so fuck it.
|
|
||||||
*/
|
|
||||||
var m = data.match(/https:\/\/www\.ustream\.tv\/embed\/(\d+)/);
|
|
||||||
if (m) {
|
|
||||||
var title = "Ustream.tv - " + id;
|
|
||||||
var media = new Media(m[1], title, "--:--", "us");
|
|
||||||
callback(false, media);
|
|
||||||
} else {
|
|
||||||
callback("Channel ID not found", null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/* rtmp stream */
|
/* rtmp stream */
|
||||||
rt: function (id, callback) {
|
rt: function (id, callback) {
|
||||||
var title = "Livestream";
|
var title = "Livestream";
|
||||||
|
@ -430,23 +302,6 @@ var Getters = {
|
||||||
callback(false, media);
|
callback(false, media);
|
||||||
},
|
},
|
||||||
|
|
||||||
/* imgur.com albums */
|
|
||||||
im: function (id, callback) {
|
|
||||||
/**
|
|
||||||
* TODO: Consider deprecating this in favor of custom embeds
|
|
||||||
*/
|
|
||||||
var m = id.match(/([\w-]+)/);
|
|
||||||
if (m) {
|
|
||||||
id = m[1];
|
|
||||||
} else {
|
|
||||||
callback("Invalid ID", null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var title = "Imgur Album - " + id;
|
|
||||||
var media = new Media(id, title, "--:--", "im");
|
|
||||||
callback(false, media);
|
|
||||||
},
|
|
||||||
|
|
||||||
/* custom embed */
|
/* custom embed */
|
||||||
cu: function (id, callback) {
|
cu: function (id, callback) {
|
||||||
var media;
|
var media;
|
||||||
|
@ -498,28 +353,6 @@ var Getters = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/* hitbox.tv / smashcast.tv */
|
|
||||||
hb: function (id, callback) {
|
|
||||||
var m = id.match(/([\w-]+)/);
|
|
||||||
if (m) {
|
|
||||||
id = m[1];
|
|
||||||
} else {
|
|
||||||
callback("Invalid ID", null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var title = "Smashcast - " + id;
|
|
||||||
var media = new Media(id, title, "--:--", "hb");
|
|
||||||
callback(false, media);
|
|
||||||
},
|
|
||||||
|
|
||||||
/* vid.me */
|
|
||||||
vm: function (id, callback) {
|
|
||||||
process.nextTick(
|
|
||||||
callback,
|
|
||||||
"As of December 2017, vid.me is no longer in service."
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/* streamable */
|
/* streamable */
|
||||||
sb: function (id, callback) {
|
sb: function (id, callback) {
|
||||||
if (!/^[\w-]+$/.test(id)) {
|
if (!/^[\w-]+$/.test(id)) {
|
||||||
|
@ -536,6 +369,16 @@ var Getters = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* PeerTube network */
|
||||||
|
pt: function (id, callback) {
|
||||||
|
PeerTube.lookup(id).then(video => {
|
||||||
|
video = new Media(video.id, video.title, video.duration, "pt", video.meta);
|
||||||
|
callback(null, video);
|
||||||
|
}).catch(error => {
|
||||||
|
callback(error.message || error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/* custom media - https://github.com/calzoneman/sync/issues/655 */
|
/* custom media - https://github.com/calzoneman/sync/issues/655 */
|
||||||
cm: async function (id, callback) {
|
cm: async function (id, callback) {
|
||||||
try {
|
try {
|
||||||
|
@ -546,12 +389,44 @@ var Getters = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/* mixer.com */
|
/* BitChute */
|
||||||
mx: function (id, callback) {
|
bc: function (id, callback) {
|
||||||
process.nextTick(
|
BitChute.lookup(id).then(video => {
|
||||||
callback,
|
video = new Media(video.id, video.title, video.duration, "bc", video.meta);
|
||||||
"As of July 2020, Mixer is no longer in service."
|
callback(null, video);
|
||||||
);
|
}).catch(error => {
|
||||||
|
callback(error.message || error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Odysee */
|
||||||
|
od: function (id, callback) {
|
||||||
|
Odysee.lookup(id).then(video => {
|
||||||
|
video = new Media(video.id, video.title, video.duration, "od", video.meta);
|
||||||
|
callback(null, video);
|
||||||
|
}).catch(error => {
|
||||||
|
callback(error.message || error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* BandCamp */
|
||||||
|
bn: function (id, callback) {
|
||||||
|
BandCamp.lookup(id).then(video => {
|
||||||
|
video = new Media(video.id, video.title, video.duration, "bn", video.meta);
|
||||||
|
callback(null, video);
|
||||||
|
}).catch(error => {
|
||||||
|
callback(error.message || error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Niconico */
|
||||||
|
nv: function (id, callback) {
|
||||||
|
Nicovideo.lookup(id).then(video => {
|
||||||
|
video = new Media(video.id, video.title, video.duration, "nv", video.meta);
|
||||||
|
callback(null, video);
|
||||||
|
}).catch(error => {
|
||||||
|
callback(error.message || error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ const cookieParser = require("cookie-parser")(Config.get("http.cookie-secret"));
|
||||||
import typecheck from 'json-typecheck';
|
import typecheck from 'json-typecheck';
|
||||||
import { isTorExit } from '../tor';
|
import { isTorExit } from '../tor';
|
||||||
import session from '../session';
|
import session from '../session';
|
||||||
import counters from '../counters';
|
|
||||||
import { verifyIPSessionCookie } from '../web/middleware/ipsessioncookie';
|
import { verifyIPSessionCookie } from '../web/middleware/ipsessioncookie';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
const verifySession = Promise.promisify(session.verifySession);
|
const verifySession = Promise.promisify(session.verifySession);
|
||||||
|
@ -15,7 +14,6 @@ const getAliases = Promise.promisify(db.getAliases);
|
||||||
import { CachingGlobalBanlist } from './globalban';
|
import { CachingGlobalBanlist } from './globalban';
|
||||||
import proxyaddr from 'proxy-addr';
|
import proxyaddr from 'proxy-addr';
|
||||||
import { Counter, Gauge } from 'prom-client';
|
import { Counter, Gauge } from 'prom-client';
|
||||||
import Socket from 'socket.io/lib/socket';
|
|
||||||
import { TokenBucket } from '../util/token-bucket';
|
import { TokenBucket } from '../util/token-bucket';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
|
|
||||||
|
@ -109,28 +107,6 @@ class IOServer {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
TODO: see https://github.com/calzoneman/sync/issues/724
|
|
||||||
ipConnectionLimitMiddleware(socket, next) {
|
|
||||||
const ip = socket.context.ipAddress;
|
|
||||||
const count = this.ipCount.get(ip) || 0;
|
|
||||||
if (count >= Config.get('io.ip-connection-limit')) {
|
|
||||||
// TODO: better error message would be nice
|
|
||||||
next(new Error('Too many connections from your IP address'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ipCount.set(ip, count + 1);
|
|
||||||
console.log(ip, this.ipCount.get(ip));
|
|
||||||
socket.once('disconnect', () => {
|
|
||||||
console.log('Disconnect event has fired for', socket.id);
|
|
||||||
this.ipCount.set(ip, this.ipCount.get(ip) - 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
checkIPLimit(socket) {
|
checkIPLimit(socket) {
|
||||||
const ip = socket.context.ipAddress;
|
const ip = socket.context.ipAddress;
|
||||||
const count = this.ipCount.get(ip) || 0;
|
const count = this.ipCount.get(ip) || 0;
|
||||||
|
@ -223,12 +199,14 @@ class IOServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patchTypecheckedFunctions(socket);
|
||||||
|
patchSocketMetrics(socket);
|
||||||
|
|
||||||
this.setRateLimiter(socket);
|
this.setRateLimiter(socket);
|
||||||
|
|
||||||
emitMetrics(socket);
|
emitMetrics(socket);
|
||||||
|
|
||||||
LOGGER.info('Accepted socket from %s', socket.context.ipAddress);
|
LOGGER.info('Accepted socket from %s', socket.context.ipAddress);
|
||||||
counters.add('socket.io:accept', 1);
|
|
||||||
socket.once('disconnect', (reason, reasonDetail) => {
|
socket.once('disconnect', (reason, reasonDetail) => {
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
'%s disconnected (%s%s)',
|
'%s disconnected (%s%s)',
|
||||||
|
@ -236,7 +214,6 @@ class IOServer {
|
||||||
reason,
|
reason,
|
||||||
reasonDetail ? ` - ${reasonDetail}` : ''
|
reasonDetail ? ` - ${reasonDetail}` : ''
|
||||||
);
|
);
|
||||||
counters.add('socket.io:disconnect', 1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = new User(socket, socket.context.ipAddress, socket.context.user);
|
const user = new User(socket, socket.context.ipAddress, socket.context.user);
|
||||||
|
@ -271,14 +248,10 @@ class IOServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
initSocketIO() {
|
initSocketIO() {
|
||||||
patchSocketMetrics();
|
|
||||||
patchTypecheckedFunctions();
|
|
||||||
|
|
||||||
const io = this.io = sio.instance = sio();
|
const io = this.io = sio.instance = sio();
|
||||||
io.use(this.ipProxyMiddleware.bind(this));
|
io.use(this.ipProxyMiddleware.bind(this));
|
||||||
io.use(this.ipBanMiddleware.bind(this));
|
io.use(this.ipBanMiddleware.bind(this));
|
||||||
io.use(this.ipThrottleMiddleware.bind(this));
|
io.use(this.ipThrottleMiddleware.bind(this));
|
||||||
//io.use(this.ipConnectionLimitMiddleware.bind(this));
|
|
||||||
io.use(this.cookieParsingMiddleware.bind(this));
|
io.use(this.cookieParsingMiddleware.bind(this));
|
||||||
io.use(this.ipSessionCookieMiddleware.bind(this));
|
io.use(this.ipSessionCookieMiddleware.bind(this));
|
||||||
io.use(this.authUserMiddleware.bind(this));
|
io.use(this.authUserMiddleware.bind(this));
|
||||||
|
@ -293,7 +266,7 @@ class IOServer {
|
||||||
const engineOpts = {
|
const engineOpts = {
|
||||||
/*
|
/*
|
||||||
* Set ping timeout to 2 minutes to avoid spurious reconnects
|
* Set ping timeout to 2 minutes to avoid spurious reconnects
|
||||||
* during transient network issues. The default of 5 minutes
|
* during transient network issues. The default of 20 seconds
|
||||||
* is too aggressive.
|
* is too aggressive.
|
||||||
*
|
*
|
||||||
* https://github.com/calzoneman/sync/issues/780
|
* https://github.com/calzoneman/sync/issues/780
|
||||||
|
@ -312,11 +285,17 @@ class IOServer {
|
||||||
perMessageDeflate: false,
|
perMessageDeflate: false,
|
||||||
httpCompression: false,
|
httpCompression: false,
|
||||||
|
|
||||||
|
maxHttpBufferSize: 1 << 20,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Default is 10MB.
|
* Enable legacy support for socket.io v2 clients (e.g., bots)
|
||||||
* Even 1MiB seems like a generous limit...
|
|
||||||
*/
|
*/
|
||||||
maxHttpBufferSize: 1 << 20
|
allowEIO3: true,
|
||||||
|
|
||||||
|
cors: {
|
||||||
|
origin: getCorsAllowCallback(),
|
||||||
|
credentials: true // enable cookies for auth
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
servers.forEach(server => {
|
servers.forEach(server => {
|
||||||
|
@ -333,26 +312,25 @@ const outgoingPacketCount = new Counter({
|
||||||
name: 'cytube_socketio_outgoing_packets_total',
|
name: 'cytube_socketio_outgoing_packets_total',
|
||||||
help: 'Number of outgoing socket.io packets to clients'
|
help: 'Number of outgoing socket.io packets to clients'
|
||||||
});
|
});
|
||||||
function patchSocketMetrics() {
|
function patchSocketMetrics(sock) {
|
||||||
const onevent = Socket.prototype.onevent;
|
|
||||||
const packet = Socket.prototype.packet;
|
|
||||||
const emit = require('events').EventEmitter.prototype.emit;
|
const emit = require('events').EventEmitter.prototype.emit;
|
||||||
|
|
||||||
Socket.prototype.onevent = function patchedOnevent() {
|
sock.onAny(() => {
|
||||||
onevent.apply(this, arguments);
|
|
||||||
incomingEventCount.inc(1);
|
incomingEventCount.inc(1);
|
||||||
emit.call(this, 'cytube:count-event');
|
emit.call(sock, 'cytube:count-event');
|
||||||
};
|
});
|
||||||
|
|
||||||
Socket.prototype.packet = function patchedPacket() {
|
let packet = sock.packet;
|
||||||
|
sock.packet = function patchedPacket() {
|
||||||
packet.apply(this, arguments);
|
packet.apply(this, arguments);
|
||||||
outgoingPacketCount.inc(1);
|
outgoingPacketCount.inc(1);
|
||||||
};
|
}.bind(sock);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: remove this crap */
|
/* TODO: remove this crap */
|
||||||
function patchTypecheckedFunctions() {
|
/* Addendum 2021-08-14: socket.io v4 supports middleware, maybe move type validation to that */
|
||||||
Socket.prototype.typecheckedOn = function typecheckedOn(msg, template, cb) {
|
function patchTypecheckedFunctions(sock) {
|
||||||
|
sock.typecheckedOn = function typecheckedOn(msg, template, cb) {
|
||||||
this.on(msg, (data, ack) => {
|
this.on(msg, (data, ack) => {
|
||||||
typecheck(data, template, (err, data) => {
|
typecheck(data, template, (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -364,9 +342,9 @@ function patchTypecheckedFunctions() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
}.bind(sock);
|
||||||
|
|
||||||
Socket.prototype.typecheckedOnce = function typecheckedOnce(msg, template, cb) {
|
sock.typecheckedOnce = function typecheckedOnce(msg, template, cb) {
|
||||||
this.once(msg, data => {
|
this.once(msg, data => {
|
||||||
typecheck(data, template, (err, data) => {
|
typecheck(data, template, (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -378,7 +356,7 @@ function patchTypecheckedFunctions() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
}.bind(sock);
|
||||||
}
|
}
|
||||||
|
|
||||||
let globalIPBanlist = null;
|
let globalIPBanlist = null;
|
||||||
|
@ -412,16 +390,17 @@ const promSocketReconnect = new Counter({
|
||||||
function emitMetrics(sock) {
|
function emitMetrics(sock) {
|
||||||
try {
|
try {
|
||||||
let closed = false;
|
let closed = false;
|
||||||
let transportName = sock.client.conn.transport.name;
|
let transportName = sock.conn.transport.name;
|
||||||
promSocketCount.inc({ transport: transportName });
|
promSocketCount.inc({ transport: transportName });
|
||||||
promSocketAccept.inc(1);
|
promSocketAccept.inc(1);
|
||||||
|
|
||||||
sock.client.conn.on('upgrade', newTransport => {
|
sock.conn.on('upgrade', () => {
|
||||||
try {
|
try {
|
||||||
|
let newTransport = sock.conn.transport.name;
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (!closed && newTransport.name !== transportName) {
|
if (!closed && newTransport !== transportName) {
|
||||||
promSocketCount.dec({ transport: transportName });
|
promSocketCount.dec({ transport: transportName });
|
||||||
transportName = newTransport.name;
|
transportName = newTransport;
|
||||||
promSocketCount.inc({ transport: transportName });
|
promSocketCount.inc({ transport: transportName });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -529,3 +508,30 @@ setInterval(function () {
|
||||||
LOGGER.info('Cleaned up %d stale IP throttle token buckets', cleaned);
|
LOGGER.info('Cleaned up %d stale IP throttle token buckets', cleaned);
|
||||||
}
|
}
|
||||||
}, 5 * 60 * 1000);
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
function getCorsAllowCallback() {
|
||||||
|
let origins = Array.prototype.slice.call(Config.get('io.cors.allowed-origins'));
|
||||||
|
|
||||||
|
origins = origins.concat([
|
||||||
|
Config.get('io.domain'),
|
||||||
|
Config.get('https.domain')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return function corsOriginAllowed(origin, callback) {
|
||||||
|
if (!origin) {
|
||||||
|
// Non-browser clients might not care about Origin, allow these.
|
||||||
|
callback(null, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different ports are technically cross-origin; a distinction that does not matter to CyTube.
|
||||||
|
origin = origin.replace(/:\d+$/, '');
|
||||||
|
|
||||||
|
if (origins.includes(origin)) {
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
LOGGER.warn('Rejecting origin "%s"; allowed origins are %j', origin, origins);
|
||||||
|
callback(new Error('Invalid origin'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
36
src/main.js
36
src/main.js
|
@ -2,6 +2,7 @@ import Config from './config';
|
||||||
import * as Switches from './switches';
|
import * as Switches from './switches';
|
||||||
import { eventlog } from './logger';
|
import { eventlog } from './logger';
|
||||||
require('source-map-support').install();
|
require('source-map-support').install();
|
||||||
|
import * as bannedChannels from './cli/banned-channels';
|
||||||
|
|
||||||
const LOGGER = require('@calzoneman/jsli')('main');
|
const LOGGER = require('@calzoneman/jsli')('main');
|
||||||
|
|
||||||
|
@ -28,10 +29,39 @@ if (!Config.get('debug')) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCliCmd(cmd) {
|
||||||
|
try {
|
||||||
|
switch (cmd.command) {
|
||||||
|
case 'ban-channel':
|
||||||
|
return bannedChannels.handleBanChannel(cmd);
|
||||||
|
case 'unban-channel':
|
||||||
|
return bannedChannels.handleUnbanChannel(cmd);
|
||||||
|
case 'show-banned-channel':
|
||||||
|
return bannedChannels.handleShowBannedChannel(cmd);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unrecognized command "${cmd.command}"`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { status: 'error', error: String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: this can probably just be part of servsock.js
|
// TODO: this can probably just be part of servsock.js
|
||||||
// servsock should also be refactored to send replies instead of
|
// servsock should also be refactored to send replies instead of
|
||||||
// relying solely on tailing logs
|
// relying solely on tailing logs
|
||||||
function handleLine(line) {
|
function handleLine(line, client) {
|
||||||
|
try {
|
||||||
|
let cmd = JSON.parse(line);
|
||||||
|
handleCliCmd(cmd).then(res => {
|
||||||
|
client.write(JSON.stringify(res) + '\n');
|
||||||
|
}).catch(error => {
|
||||||
|
LOGGER.error(`Unexpected error in handleCliCmd: ${error.stack}`);
|
||||||
|
client.write('{"status":"error","error":"internal error"}\n');
|
||||||
|
});
|
||||||
|
} catch (_error) {
|
||||||
|
// eslint no-empty: off
|
||||||
|
}
|
||||||
|
|
||||||
if (line === '/reload') {
|
if (line === '/reload') {
|
||||||
LOGGER.info('Reloading config');
|
LOGGER.info('Reloading config');
|
||||||
try {
|
try {
|
||||||
|
@ -81,9 +111,9 @@ if (Config.get('service-socket.enabled')) {
|
||||||
const ServiceSocket = require('./servsock');
|
const ServiceSocket = require('./servsock');
|
||||||
const sock = new ServiceSocket();
|
const sock = new ServiceSocket();
|
||||||
sock.init(
|
sock.init(
|
||||||
line => {
|
(line, client) => {
|
||||||
try {
|
try {
|
||||||
handleLine(line);
|
handleLine(line, client);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Error in UNIX socket command handler: %s',
|
'Error in UNIX socket command handler: %s',
|
||||||
|
|
|
@ -39,7 +39,7 @@ Media.prototype = {
|
||||||
embed: this.meta.embed,
|
embed: this.meta.embed,
|
||||||
gdrive_subtitles: this.meta.gdrive_subtitles,
|
gdrive_subtitles: this.meta.gdrive_subtitles,
|
||||||
textTracks: this.meta.textTracks,
|
textTracks: this.meta.textTracks,
|
||||||
mixer: this.meta.mixer
|
audioTracks: this.meta.audioTracks
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,6 +54,11 @@ Media.prototype = {
|
||||||
result.meta.direct = this.meta.direct;
|
result.meta.direct = this.meta.direct;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only save thumbnails for items which can be audio track only
|
||||||
|
if (['bn','cm'].includes(this.type)) {
|
||||||
|
result.meta.thumbnail = this.meta.thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
/** MetricsReporter that records metrics as JSON objects in a file, one per line */
|
|
||||||
class JSONFileMetricsReporter {
|
|
||||||
/**
|
|
||||||
* Create a new JSONFileMetricsReporter that writes to the given file path.
|
|
||||||
*
|
|
||||||
* @param {string} filename file path to write to
|
|
||||||
*/
|
|
||||||
constructor(filename) {
|
|
||||||
this.writeStream = fs.createWriteStream(filename, { flags: 'a' });
|
|
||||||
this.metrics = {};
|
|
||||||
this.timers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see {@link module:cytube-common/metrics/metrics.incCounter}
|
|
||||||
*/
|
|
||||||
incCounter(counter, value) {
|
|
||||||
if (!this.metrics.hasOwnProperty(counter)) {
|
|
||||||
this.metrics[counter] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.metrics[counter] += value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a time metric
|
|
||||||
*
|
|
||||||
* @param {string} timer name of the timer
|
|
||||||
* @param {number} ms milliseconds to record
|
|
||||||
*/
|
|
||||||
addTime(timer, ms) {
|
|
||||||
if (!this.timers.hasOwnProperty(timer)) {
|
|
||||||
this.timers[timer] = {
|
|
||||||
totalTime: 0,
|
|
||||||
count: 0,
|
|
||||||
p100: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timers[timer].totalTime += ms;
|
|
||||||
this.timers[timer].count++;
|
|
||||||
if (ms > this.timers[timer].p100) {
|
|
||||||
this.timers[timer].p100 = ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see {@link module:cytube-common/metrics/metrics.addProperty}
|
|
||||||
*/
|
|
||||||
addProperty(property, value) {
|
|
||||||
this.metrics[property] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
report() {
|
|
||||||
for (const timer in this.timers) {
|
|
||||||
this.metrics[timer+':avg'] = this.timers[timer].totalTime / this.timers[timer].count;
|
|
||||||
this.metrics[timer+':count'] = this.timers[timer].count;
|
|
||||||
this.metrics[timer+':p100'] = this.timers[timer].p100;
|
|
||||||
}
|
|
||||||
|
|
||||||
const line = JSON.stringify(this.metrics) + '\n';
|
|
||||||
try {
|
|
||||||
this.writeStream.write(line);
|
|
||||||
} finally {
|
|
||||||
this.metrics = {};
|
|
||||||
this.timers = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { JSONFileMetricsReporter };
|
|
|
@ -1,136 +0,0 @@
|
||||||
import os from 'os';
|
|
||||||
|
|
||||||
/** @module cytube-common/metrics/metrics */
|
|
||||||
|
|
||||||
const MEM_RSS = 'memory:rss';
|
|
||||||
const LOAD_1MIN = 'load:1min';
|
|
||||||
const TIMESTAMP = 'time';
|
|
||||||
const logger = require('@calzoneman/jsli')('metrics');
|
|
||||||
|
|
||||||
var delegate = null;
|
|
||||||
var reportInterval = null;
|
|
||||||
var reportHooks = [];
|
|
||||||
let warnedNoReporter = false;
|
|
||||||
|
|
||||||
function warnNoReporter() {
|
|
||||||
if (!warnedNoReporter) {
|
|
||||||
warnedNoReporter = true;
|
|
||||||
logger.warn('No metrics reporter configured. Metrics will not be recorded.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increment a metrics counter by the specified amount.
|
|
||||||
*
|
|
||||||
* @param {string} counter name of the counter to increment
|
|
||||||
* @param {number} value optional value to increment by (default 1)
|
|
||||||
*/
|
|
||||||
export function incCounter(counter, amount = 1) {
|
|
||||||
if (delegate === null) {
|
|
||||||
warnNoReporter();
|
|
||||||
} else {
|
|
||||||
delegate.incCounter(counter, amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a timer. Returns a handle to use to end the timer.
|
|
||||||
*
|
|
||||||
* @param {string} timer name
|
|
||||||
* @return {object} timer handle
|
|
||||||
*/
|
|
||||||
export function startTimer(timer) {
|
|
||||||
return {
|
|
||||||
timer: timer,
|
|
||||||
hrtime: process.hrtime()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop a timer and record the time (as an average)
|
|
||||||
*
|
|
||||||
* @param {object} handle timer handle to Stop
|
|
||||||
*/
|
|
||||||
export function stopTimer(handle) {
|
|
||||||
if (delegate === null) {
|
|
||||||
warnNoReporter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [seconds, ns] = process.hrtime(handle.hrtime);
|
|
||||||
delegate.addTime(handle.timer, seconds*1e3 + ns/1e6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a property to the current metrics period.
|
|
||||||
*
|
|
||||||
* @param {string} property property name to add
|
|
||||||
* @param {any} property value
|
|
||||||
*/
|
|
||||||
export function addProperty(property, value) {
|
|
||||||
if (delegate === null) {
|
|
||||||
warnNoReporter();
|
|
||||||
} else {
|
|
||||||
delegate.addProperty(property, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the metrics reporter to record to.
|
|
||||||
*
|
|
||||||
* @param {MetricsReporter} reporter reporter to record metrics to
|
|
||||||
*/
|
|
||||||
export function setReporter(reporter) {
|
|
||||||
delegate = reporter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the interval at which to report metrics.
|
|
||||||
*
|
|
||||||
* @param {number} interval time in milliseconds between successive reports
|
|
||||||
*/
|
|
||||||
export function setReportInterval(interval) {
|
|
||||||
clearInterval(reportInterval);
|
|
||||||
if (!isNaN(interval) && interval >= 0) {
|
|
||||||
reportInterval = setInterval(reportLoop, interval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a callback to add additional metrics before reporting.
|
|
||||||
*
|
|
||||||
* @param {function(metricsReporter)} hook callback to be invoked before reporting
|
|
||||||
*/
|
|
||||||
export function addReportHook(hook) {
|
|
||||||
reportHooks.push(hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearReportHooks() {
|
|
||||||
reportHooks = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force metrics to be reported right now.
|
|
||||||
*/
|
|
||||||
export function flush() {
|
|
||||||
reportLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDefaults() {
|
|
||||||
addProperty(MEM_RSS, process.memoryUsage().rss / 1048576);
|
|
||||||
addProperty(LOAD_1MIN, os.loadavg()[0]);
|
|
||||||
addProperty(TIMESTAMP, new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportLoop() {
|
|
||||||
if (delegate !== null) {
|
|
||||||
try {
|
|
||||||
addDefaults();
|
|
||||||
reportHooks.forEach(hook => {
|
|
||||||
hook(delegate);
|
|
||||||
});
|
|
||||||
delegate.report();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
29
src/peertubelist.js
Normal file
29
src/peertubelist.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { fetchPeertubeDomains, setDomains } from '@cytube/mediaquery/lib/provider/peertube';
|
||||||
|
import { stat, readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const LOGGER = require('@calzoneman/jsli')('peertubelist');
|
||||||
|
const ONE_DAY = 24 * 3600 * 1000;
|
||||||
|
const FILENAME = path.join(__dirname, '..', 'peertube-hosts.json');
|
||||||
|
|
||||||
|
export async function setupPeertubeDomains() {
|
||||||
|
try {
|
||||||
|
let mtime;
|
||||||
|
try {
|
||||||
|
mtime = (await stat(FILENAME)).mtime;
|
||||||
|
} catch (_error) {
|
||||||
|
mtime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - mtime > ONE_DAY) {
|
||||||
|
LOGGER.info('Updating peertube host list');
|
||||||
|
const hosts = await fetchPeertubeDomains();
|
||||||
|
await writeFile(FILENAME, JSON.stringify(hosts));
|
||||||
|
}
|
||||||
|
|
||||||
|
const hosts = JSON.parse(await readFile(FILENAME));
|
||||||
|
setDomains(hosts);
|
||||||
|
} catch (error) {
|
||||||
|
LOGGER.error('Failed to initialize peertube host list: %s', error.stack);
|
||||||
|
}
|
||||||
|
}
|
135
src/poll.js
135
src/poll.js
|
@ -1,58 +1,103 @@
|
||||||
const link = /(\w+:\/\/(?:[^:/[\]\s]+|\[[0-9a-f:]+\])(?::\d+)?(?:\/[^/\s]*)*)/ig;
|
const link = /(\w+:\/\/(?:[^:/[\]\s]+|\[[0-9a-f:]+\])(?::\d+)?(?:\/[^/\s]*)*)/ig;
|
||||||
var XSS = require("./xss");
|
const XSS = require('./xss');
|
||||||
|
|
||||||
var Poll = function(initiator, title, options, obscured) {
|
function sanitizedWithLinksReplaced(text) {
|
||||||
this.initiator = initiator;
|
return XSS.sanitizeText(text)
|
||||||
title = XSS.sanitizeText(title);
|
.replace(link, '<a href="$1" target="_blank" rel="noopener noreferer">$1</a>');
|
||||||
this.title = title.replace(link, "<a href=\"$1\" target=\"_blank\">$1</a>");
|
}
|
||||||
this.options = options;
|
|
||||||
for (let i = 0; i < this.options.length; i++) {
|
|
||||||
this.options[i] = XSS.sanitizeText(this.options[i]);
|
|
||||||
this.options[i] = this.options[i].replace(link, "<a href=\"$1\" target=\"_blank\">$1</a>");
|
|
||||||
|
|
||||||
|
class Poll {
|
||||||
|
static create(createdBy, title, choices, options = { hideVotes: false, retainVotes: false }) {
|
||||||
|
let poll = new Poll();
|
||||||
|
poll.createdAt = new Date();
|
||||||
|
poll.createdBy = createdBy;
|
||||||
|
poll.title = sanitizedWithLinksReplaced(title);
|
||||||
|
poll.choices = choices.map(choice => sanitizedWithLinksReplaced(choice));
|
||||||
|
poll.hideVotes = options.hideVotes;
|
||||||
|
poll.retainVotes = options.retainVotes;
|
||||||
|
poll.votes = new Map();
|
||||||
|
return poll;
|
||||||
}
|
}
|
||||||
this.obscured = obscured || false;
|
|
||||||
this.counts = new Array(options.length);
|
|
||||||
for(let i = 0; i < this.counts.length; i++) {
|
|
||||||
this.counts[i] = 0;
|
|
||||||
}
|
|
||||||
this.votes = {};
|
|
||||||
this.timestamp = Date.now();
|
|
||||||
};
|
|
||||||
|
|
||||||
Poll.prototype.vote = function(ip, option) {
|
static fromChannelData({ initiator, title, options, _counts, votes, timestamp, obscured, retainVotes }) {
|
||||||
if(!(ip in this.votes) || this.votes[ip] == null) {
|
let poll = new Poll();
|
||||||
this.votes[ip] = option;
|
if (timestamp === undefined) // Very old polls still in the database lack timestamps
|
||||||
this.counts[option]++;
|
timestamp = Date.now();
|
||||||
return true;
|
poll.createdAt = new Date(timestamp);
|
||||||
|
poll.createdBy = initiator;
|
||||||
|
poll.title = title;
|
||||||
|
poll.choices = options;
|
||||||
|
poll.votes = new Map();
|
||||||
|
Object.keys(votes).forEach(key => {
|
||||||
|
if (votes[key] !== null)
|
||||||
|
poll.votes.set(key, votes[key]);
|
||||||
|
});
|
||||||
|
poll.hideVotes = obscured;
|
||||||
|
poll.retainVotes = retainVotes || false;
|
||||||
|
return poll;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
Poll.prototype.unvote = function(ip) {
|
toChannelData() {
|
||||||
if(ip in this.votes && this.votes[ip] != null) {
|
let counts = new Array(this.choices.length);
|
||||||
this.counts[this.votes[ip]]--;
|
counts.fill(0);
|
||||||
this.votes[ip] = null;
|
|
||||||
|
// TODO: it would be desirable one day to move away from using an Object here.
|
||||||
|
// This is just for backwards-compatibility with the existing format.
|
||||||
|
let votes = {};
|
||||||
|
|
||||||
|
this.votes.forEach((index, key) => {
|
||||||
|
votes[key] = index;
|
||||||
|
counts[index]++;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
initiator: this.createdBy,
|
||||||
|
options: this.choices,
|
||||||
|
counts,
|
||||||
|
votes,
|
||||||
|
obscured: this.hideVotes,
|
||||||
|
retainVotes: this.retainVotes,
|
||||||
|
timestamp: this.createdAt.getTime()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
Poll.prototype.packUpdate = function (showhidden) {
|
countVote(key, choiceId) {
|
||||||
var counts = Array.prototype.slice.call(this.counts);
|
if (choiceId < 0 || choiceId >= this.choices.length)
|
||||||
if (this.obscured) {
|
return false;
|
||||||
for(var i = 0; i < counts.length; i++) {
|
|
||||||
if (!showhidden)
|
let changed = !this.votes.has(key) || this.votes.get(key) !== choiceId;
|
||||||
counts[i] = "";
|
this.votes.set(key, choiceId);
|
||||||
counts[i] += "?";
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
uncountVote(key) {
|
||||||
|
let changed = this.votes.has(key);
|
||||||
|
this.votes.delete(key);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
toUpdateFrame(showHiddenVotes) {
|
||||||
|
let counts = new Array(this.choices.length);
|
||||||
|
counts.fill(0);
|
||||||
|
|
||||||
|
this.votes.forEach(index => counts[index]++);
|
||||||
|
|
||||||
|
if (this.hideVotes) {
|
||||||
|
counts = counts.map(c => {
|
||||||
|
if (showHiddenVotes) return `${c}?`;
|
||||||
|
else return '?';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
options: this.choices,
|
||||||
|
counts: counts,
|
||||||
|
initiator: this.createdBy,
|
||||||
|
timestamp: this.createdAt.getTime()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
var packed = {
|
}
|
||||||
title: this.title,
|
|
||||||
options: this.options,
|
|
||||||
counts: counts,
|
|
||||||
initiator: this.initiator,
|
|
||||||
timestamp: this.timestamp
|
|
||||||
};
|
|
||||||
return packed;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Poll = Poll;
|
exports.Poll = Poll;
|
||||||
|
|
|
@ -48,6 +48,7 @@ import { PartitionModule } from './partition/partitionmodule';
|
||||||
import { Gauge } from 'prom-client';
|
import { Gauge } from 'prom-client';
|
||||||
import { EmailController } from './controller/email';
|
import { EmailController } from './controller/email';
|
||||||
import { CaptchaController } from './controller/captcha';
|
import { CaptchaController } from './controller/captcha';
|
||||||
|
import { BannedChannelsController } from './controller/banned-channels';
|
||||||
|
|
||||||
var Server = function () {
|
var Server = function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -71,6 +72,7 @@ var Server = function () {
|
||||||
const globalMessageBus = this.initModule.getGlobalMessageBus();
|
const globalMessageBus = this.initModule.getGlobalMessageBus();
|
||||||
globalMessageBus.on('UserProfileChanged', this.handleUserProfileChange.bind(this));
|
globalMessageBus.on('UserProfileChanged', this.handleUserProfileChange.bind(this));
|
||||||
globalMessageBus.on('ChannelDeleted', this.handleChannelDelete.bind(this));
|
globalMessageBus.on('ChannelDeleted', this.handleChannelDelete.bind(this));
|
||||||
|
globalMessageBus.on('ChannelBanned', this.handleChannelBanned.bind(this));
|
||||||
globalMessageBus.on('ChannelRegistered', this.handleChannelRegister.bind(this));
|
globalMessageBus.on('ChannelRegistered', this.handleChannelRegister.bind(this));
|
||||||
|
|
||||||
// database init ------------------------------------------------------
|
// database init ------------------------------------------------------
|
||||||
|
@ -108,6 +110,11 @@ var Server = function () {
|
||||||
Config.getCaptchaConfig()
|
Config.getCaptchaConfig()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.bannedChannelsController = new BannedChannelsController(
|
||||||
|
self.db.channels,
|
||||||
|
globalMessageBus
|
||||||
|
);
|
||||||
|
|
||||||
// webserver init -----------------------------------------------------
|
// webserver init -----------------------------------------------------
|
||||||
const ioConfig = IOConfiguration.fromOldConfig(Config);
|
const ioConfig = IOConfiguration.fromOldConfig(Config);
|
||||||
const webConfig = WebConfiguration.fromOldConfig(Config);
|
const webConfig = WebConfiguration.fromOldConfig(Config);
|
||||||
|
@ -134,7 +141,8 @@ var Server = function () {
|
||||||
Config.getEmailConfig(),
|
Config.getEmailConfig(),
|
||||||
emailController,
|
emailController,
|
||||||
Config.getCaptchaConfig(),
|
Config.getCaptchaConfig(),
|
||||||
captchaController
|
captchaController,
|
||||||
|
self.bannedChannelsController
|
||||||
);
|
);
|
||||||
|
|
||||||
// http/https/sio server init -----------------------------------------
|
// http/https/sio server init -----------------------------------------
|
||||||
|
@ -204,6 +212,8 @@ var Server = function () {
|
||||||
// background tasks init ----------------------------------------------
|
// background tasks init ----------------------------------------------
|
||||||
require("./bgtask")(self);
|
require("./bgtask")(self);
|
||||||
|
|
||||||
|
require("./peertubelist").setupPeertubeDomains().then(() => {});
|
||||||
|
|
||||||
// prometheus server
|
// prometheus server
|
||||||
const prometheusConfig = Config.getPrometheusConfig();
|
const prometheusConfig = Config.getPrometheusConfig();
|
||||||
if (prometheusConfig.isEnabled()) {
|
if (prometheusConfig.isEnabled()) {
|
||||||
|
@ -547,6 +557,34 @@ Server.prototype.handleChannelDelete = function (event) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Server.prototype.handleChannelBanned = function (event) {
|
||||||
|
try {
|
||||||
|
const lname = event.channel.toLowerCase();
|
||||||
|
const reason = event.externalReason;
|
||||||
|
|
||||||
|
this.channels.forEach(channel => {
|
||||||
|
if (channel.dead) return;
|
||||||
|
|
||||||
|
if (channel.uniqueName === lname) {
|
||||||
|
channel.clearFlag(Flags.C_REGISTERED);
|
||||||
|
|
||||||
|
const users = Array.prototype.slice.call(channel.users);
|
||||||
|
users.forEach(u => {
|
||||||
|
u.kick(`Channel was banned: ${reason}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!channel.dead && !channel.dying) {
|
||||||
|
channel.emit('empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info('Processed banned channel %s', lname);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
LOGGER.error('handleChannelBanned failed: %s', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Server.prototype.handleChannelRegister = function (event) {
|
Server.prototype.handleChannelRegister = function (event) {
|
||||||
try {
|
try {
|
||||||
const lname = event.channel.toLowerCase();
|
const lname = event.channel.toLowerCase();
|
||||||
|
|
|
@ -34,7 +34,7 @@ export default class ServiceSocket {
|
||||||
delete this.connections[id];
|
delete this.connections[id];
|
||||||
});
|
});
|
||||||
stream.on('data', (msg) => {
|
stream.on('data', (msg) => {
|
||||||
this.handler(msg.toString());
|
this.handler(msg.toString(), stream);
|
||||||
});
|
});
|
||||||
}).listen(this.socket);
|
}).listen(this.socket);
|
||||||
process.on('exit', this.closeServiceSocket.bind(this));
|
process.on('exit', this.closeServiceSocket.bind(this));
|
||||||
|
|
|
@ -76,10 +76,6 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
data.name = data.name.toLowerCase();
|
data.name = data.name.toLowerCase();
|
||||||
if (data.name in Config.get("channel-blacklist")) {
|
|
||||||
this.kick("This channel is blacklisted.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.waitFlag(Flags.U_READY, () => {
|
this.waitFlag(Flags.U_READY, () => {
|
||||||
var chan;
|
var chan;
|
||||||
|
@ -102,10 +98,6 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) {
|
||||||
|
|
||||||
if (!chan.is(Flags.C_READY)) {
|
if (!chan.is(Flags.C_READY)) {
|
||||||
chan.once("loadFail", reason => {
|
chan.once("loadFail", reason => {
|
||||||
this.socket.emit("errorMsg", {
|
|
||||||
msg: reason,
|
|
||||||
alert: true
|
|
||||||
});
|
|
||||||
this.kick(`Channel could not be loaded: ${reason}`);
|
this.kick(`Channel could not be loaded: ${reason}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
45
src/util/simple-cache.js
Normal file
45
src/util/simple-cache.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
class SimpleCache {
|
||||||
|
constructor({ maxElem, maxAge }) {
|
||||||
|
this.maxElem = maxElem;
|
||||||
|
this.maxAge = maxAge;
|
||||||
|
this.cache = new Map();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
this.cleanup();
|
||||||
|
}, maxAge).unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
put(key, value) {
|
||||||
|
this.cache.set(key, { value: value, at: Date.now() });
|
||||||
|
|
||||||
|
if (this.cache.size > this.maxElem) {
|
||||||
|
this.cache.delete(this.cache.keys().next().value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
let val = this.cache.get(key);
|
||||||
|
|
||||||
|
if (val != null && Date.now() < val.at + this.maxAge) {
|
||||||
|
return val.value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
let now = Date.now();
|
||||||
|
|
||||||
|
for (let [key, value] of this.cache) {
|
||||||
|
if (value.at < now - this.maxAge) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SimpleCache };
|
|
@ -177,7 +177,7 @@
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
root.formatLink = function (id, type, meta) {
|
root.formatLink = function (id, type, _meta) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "yt":
|
case "yt":
|
||||||
return "https://youtu.be/" + id;
|
return "https://youtu.be/" + id;
|
||||||
|
@ -193,16 +193,10 @@
|
||||||
return "https://twitch.tv/" + id;
|
return "https://twitch.tv/" + id;
|
||||||
case "rt":
|
case "rt":
|
||||||
return id;
|
return id;
|
||||||
case "im":
|
|
||||||
return "https://imgur.com/a/" + id;
|
|
||||||
case "us":
|
|
||||||
return "https://ustream.tv/channel/" + id;
|
|
||||||
case "gd":
|
case "gd":
|
||||||
return "https://docs.google.com/file/d/" + id;
|
return "https://docs.google.com/file/d/" + id;
|
||||||
case "fi":
|
case "fi":
|
||||||
return id;
|
return id;
|
||||||
case "hb":
|
|
||||||
return "https://www.smashcast.tv/" + id;
|
|
||||||
case "hl":
|
case "hl":
|
||||||
return id;
|
return id;
|
||||||
case "sb":
|
case "sb":
|
||||||
|
@ -211,12 +205,22 @@
|
||||||
return "https://clips.twitch.tv/" + id;
|
return "https://clips.twitch.tv/" + id;
|
||||||
case "cm":
|
case "cm":
|
||||||
return id;
|
return id;
|
||||||
case "mx":
|
case "pt": {
|
||||||
if (meta !== null) {
|
const [domain,uuid] = id.split(';');
|
||||||
return `https://mixer.com/${meta.mixer.channelToken}`;
|
return `https://${domain}/videos/watch/${uuid}`;
|
||||||
} else {
|
}
|
||||||
return `https://mixer.com/${id}`;
|
case "bc":
|
||||||
}
|
return `https://www.bitchute.com/video/${id}/`;
|
||||||
|
case "bn": {
|
||||||
|
const [artist,track] = id.split(';');
|
||||||
|
return `https://${artist}.bandcamp.com/track/${track}`;
|
||||||
|
}
|
||||||
|
case "od": {
|
||||||
|
const [user,video] = id.split(';');
|
||||||
|
return `https://odysee.com/@${user}/${video}`;
|
||||||
|
}
|
||||||
|
case "nv":
|
||||||
|
return `https://www.nicovideo.jp/watch/${id}`;
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -226,13 +230,9 @@
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "li":
|
case "li":
|
||||||
case "tw":
|
case "tw":
|
||||||
case "us":
|
|
||||||
case "rt":
|
case "rt":
|
||||||
case "cu":
|
case "cu":
|
||||||
case "im":
|
|
||||||
case "hb":
|
|
||||||
case "hl":
|
case "hl":
|
||||||
case "mx":
|
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -265,6 +265,8 @@ async function handleNewChannel(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let banInfo = await db.channels.getBannedChannel(name);
|
||||||
|
|
||||||
db.channels.listUserChannels(user.name, function (err, channels) {
|
db.channels.listUserChannels(user.name, function (err, channels) {
|
||||||
if (err) {
|
if (err) {
|
||||||
sendPug(res, "account-channels", {
|
sendPug(res, "account-channels", {
|
||||||
|
@ -274,6 +276,14 @@ async function handleNewChannel(req, res) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (banInfo !== null) {
|
||||||
|
sendPug(res, "account-channels", {
|
||||||
|
channels: channels,
|
||||||
|
newChannelError: `Cannot register "${name}": this channel is banned.`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (name.match(Config.get("reserved-names.channels"))) {
|
if (name.match(Config.get("reserved-names.channels"))) {
|
||||||
sendPug(res, "account-channels", {
|
sendPug(res, "account-channels", {
|
||||||
channels: channels,
|
channels: channels,
|
||||||
|
@ -631,7 +641,43 @@ function handlePasswordReset(req, res) {
|
||||||
/**
|
/**
|
||||||
* Handles a request for /account/passwordrecover/<hash>
|
* Handles a request for /account/passwordrecover/<hash>
|
||||||
*/
|
*/
|
||||||
function handlePasswordRecover(req, res) {
|
function handleGetPasswordRecover(req, res) {
|
||||||
|
var hash = req.params.hash;
|
||||||
|
if (typeof hash !== "string") {
|
||||||
|
res.send(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.lookupPasswordReset(hash, function (err, row) {
|
||||||
|
if (err) {
|
||||||
|
sendPug(res, "account-passwordrecover", {
|
||||||
|
recovered: false,
|
||||||
|
recoverErr: err
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() >= row.expire) {
|
||||||
|
sendPug(res, "account-passwordrecover", {
|
||||||
|
recovered: false,
|
||||||
|
recoverErr: "This password recovery link has expired. Password " +
|
||||||
|
"recovery links are valid only for 24 hours after " +
|
||||||
|
"submission."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPug(res, "account-passwordrecover", {
|
||||||
|
confirm: true,
|
||||||
|
recovered: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a POST request for /account/passwordrecover/<hash>
|
||||||
|
*/
|
||||||
|
function handlePostPasswordRecover(req, res) {
|
||||||
var hash = req.params.hash;
|
var hash = req.params.hash;
|
||||||
if (typeof hash !== "string") {
|
if (typeof hash !== "string") {
|
||||||
res.send(400);
|
res.send(400);
|
||||||
|
@ -703,7 +749,8 @@ module.exports = {
|
||||||
app.post("/account/profile", handleAccountProfile);
|
app.post("/account/profile", handleAccountProfile);
|
||||||
app.get("/account/passwordreset", handlePasswordResetPage);
|
app.get("/account/passwordreset", handlePasswordResetPage);
|
||||||
app.post("/account/passwordreset", handlePasswordReset);
|
app.post("/account/passwordreset", handlePasswordReset);
|
||||||
app.get("/account/passwordrecover/:hash", handlePasswordRecover);
|
app.get("/account/passwordrecover/:hash", handleGetPasswordRecover);
|
||||||
|
app.post("/account/passwordrecover/:hash", handlePostPasswordRecover);
|
||||||
app.get("/account", function (req, res) {
|
app.get("/account", function (req, res) {
|
||||||
res.redirect("/login");
|
res.redirect("/login");
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
import CyTubeUtil from '../../utilities';
|
import CyTubeUtil from '../../utilities';
|
||||||
|
import Config from '../../config';
|
||||||
import { sanitizeText } from '../../xss';
|
import { sanitizeText } from '../../xss';
|
||||||
import { sendPug } from '../pug';
|
import { sendPug } from '../pug';
|
||||||
import * as HTTPStatus from '../httpstatus';
|
import * as HTTPStatus from '../httpstatus';
|
||||||
import { HTTPError } from '../../errors';
|
import { HTTPError } from '../../errors';
|
||||||
|
|
||||||
export default function initialize(app, ioConfig, chanPath) {
|
export default function initialize(app, ioConfig, chanPath, getBannedChannel) {
|
||||||
app.get(`/${chanPath}/:channel`, (req, res) => {
|
app.get(`/${chanPath}/:channel`, async (req, res) => {
|
||||||
if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) {
|
if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) {
|
||||||
throw new HTTPError(`"${sanitizeText(req.params.channel)}" is not a valid ` +
|
throw new HTTPError(`"${sanitizeText(req.params.channel)}" is not a valid ` +
|
||||||
'channel name.', { status: HTTPStatus.NOT_FOUND });
|
'channel name.', { status: HTTPStatus.NOT_FOUND });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let banInfo = await getBannedChannel(req.params.channel);
|
||||||
|
if (banInfo !== null) {
|
||||||
|
sendPug(res, 'banned_channel', {
|
||||||
|
externalReason: banInfo.externalReason
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const endpoints = ioConfig.getSocketEndpoints();
|
const endpoints = ioConfig.getSocketEndpoints();
|
||||||
if (endpoints.length === 0) {
|
if (endpoints.length === 0) {
|
||||||
throw new HTTPError('No socket.io endpoints configured');
|
throw new HTTPError('No socket.io endpoints configured');
|
||||||
|
@ -19,7 +28,8 @@ export default function initialize(app, ioConfig, chanPath) {
|
||||||
|
|
||||||
sendPug(res, 'channel', {
|
sendPug(res, 'channel', {
|
||||||
channelName: req.params.channel,
|
channelName: req.params.channel,
|
||||||
sioSource: `${socketBaseURL}/socket.io/socket.io.js`
|
sioSource: `${socketBaseURL}/socket.io/socket.io.js`,
|
||||||
|
maxMsgLen: Config.get("max-chat-message-length")
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
7
src/web/routes/iframe.js
Normal file
7
src/web/routes/iframe.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { sendPug } from '../pug';
|
||||||
|
|
||||||
|
export default function initialize(app) {
|
||||||
|
app.get('/iframe', (req, res) => {
|
||||||
|
return sendPug(res, 'iframe');
|
||||||
|
});
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ import morgan from 'morgan';
|
||||||
import csrf from './csrf';
|
import csrf from './csrf';
|
||||||
import * as HTTPStatus from './httpstatus';
|
import * as HTTPStatus from './httpstatus';
|
||||||
import { CSRFError, HTTPError } from '../errors';
|
import { CSRFError, HTTPError } from '../errors';
|
||||||
import counters from '../counters';
|
|
||||||
import { Summary, Counter } from 'prom-client';
|
import { Summary, Counter } from 'prom-client';
|
||||||
import session from '../session';
|
import session from '../session';
|
||||||
const verifySessionAsync = require('bluebird').promisify(session.verifySession);
|
const verifySessionAsync = require('bluebird').promisify(session.verifySession);
|
||||||
|
@ -144,16 +143,13 @@ module.exports = {
|
||||||
emailConfig,
|
emailConfig,
|
||||||
emailController,
|
emailController,
|
||||||
captchaConfig,
|
captchaConfig,
|
||||||
captchaController
|
captchaController,
|
||||||
|
bannedChannelsController
|
||||||
) {
|
) {
|
||||||
patchExpressToHandleAsync();
|
patchExpressToHandleAsync();
|
||||||
const chanPath = Config.get('channel-path');
|
const chanPath = Config.get('channel-path');
|
||||||
|
|
||||||
initPrometheus(app);
|
initPrometheus(app);
|
||||||
app.use((req, res, next) => {
|
|
||||||
counters.add("http:request", 1);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
require('./middleware/x-forwarded-for').initialize(app, webConfig);
|
require('./middleware/x-forwarded-for').initialize(app, webConfig);
|
||||||
app.use(bodyParser.urlencoded({
|
app.use(bodyParser.urlencoded({
|
||||||
extended: false,
|
extended: false,
|
||||||
|
@ -199,7 +195,12 @@ module.exports = {
|
||||||
LOGGER.info('Enabled express-minify for CSS and JS');
|
LOGGER.info('Enabled express-minify for CSS and JS');
|
||||||
}
|
}
|
||||||
|
|
||||||
require('./routes/channel')(app, ioConfig, chanPath);
|
require('./routes/channel')(
|
||||||
|
app,
|
||||||
|
ioConfig,
|
||||||
|
chanPath,
|
||||||
|
async name => bannedChannelsController.getBannedChannel(name)
|
||||||
|
);
|
||||||
require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries());
|
require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries());
|
||||||
require('./routes/socketconfig')(app, clusterClient);
|
require('./routes/socketconfig')(app, clusterClient);
|
||||||
require('./routes/contact')(app, webConfig);
|
require('./routes/contact')(app, webConfig);
|
||||||
|
@ -217,6 +218,7 @@ module.exports = {
|
||||||
require('./acp').init(app, ioConfig);
|
require('./acp').init(app, ioConfig);
|
||||||
require('../google2vtt').attach(app);
|
require('../google2vtt').attach(app);
|
||||||
require('./routes/google_drive_userscript')(app);
|
require('./routes/google_drive_userscript')(app);
|
||||||
|
require('./routes/iframe')(app);
|
||||||
|
|
||||||
app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), {
|
app.use(serveStatic(path.join(__dirname, '..', '..', 'www'), {
|
||||||
maxAge: webConfig.getCacheTTL()
|
maxAge: webConfig.getCacheTTL()
|
||||||
|
|
|
@ -24,7 +24,7 @@ block content
|
||||||
tbody
|
tbody
|
||||||
for c in channels
|
for c in channels
|
||||||
tr
|
tr
|
||||||
th
|
td
|
||||||
form.form-inline.pull-right(action="/account/channels", method="post", onsubmit="return confirm('Are you sure you want to delete " +c.name+ "? This cannot be undone');")
|
form.form-inline.pull-right(action="/account/channels", method="post", onsubmit="return confirm('Are you sure you want to delete " +c.name+ "? This cannot be undone');")
|
||||||
input(type="hidden", name="_csrf", value=csrfToken)
|
input(type="hidden", name="_csrf", value=csrfToken)
|
||||||
input(type="hidden", name="action", value="delete_channel")
|
input(type="hidden", name="action", value="delete_channel")
|
||||||
|
@ -32,6 +32,9 @@ block content
|
||||||
button.btn.btn-xs.btn-danger(type="submit") Delete
|
button.btn.btn-xs.btn-danger(type="submit") Delete
|
||||||
span.glyphicon.glyphicon-trash
|
span.glyphicon.glyphicon-trash
|
||||||
a(href=`/${channelPath}/${c.name}`, style="margin-left: 5px")= c.name
|
a(href=`/${channelPath}/${c.name}`, style="margin-left: 5px")= c.name
|
||||||
|
if c.banReason != null
|
||||||
|
|
|
||||||
|
span.label.label-danger Banned
|
||||||
.col-lg-6.col-md-6
|
.col-lg-6.col-md-6
|
||||||
h3 Register a new channel
|
h3 Register a new channel
|
||||||
if newChannelError
|
if newChannelError
|
||||||
|
|
|
@ -7,6 +7,9 @@ block content
|
||||||
.alert.alert-success.center.messagebox
|
.alert.alert-success.center.messagebox
|
||||||
strong Your password has been changed
|
strong Your password has been changed
|
||||||
p Your account has been assigned the temporary password <code>#{recoverPw}</code>. You may now use this password to log in and choose a new password by visiting the <a href="/account/edit">change password/email</a> page.
|
p Your account has been assigned the temporary password <code>#{recoverPw}</code>. You may now use this password to log in and choose a new password by visiting the <a href="/account/edit">change password/email</a> page.
|
||||||
|
else if confirm
|
||||||
|
form(role="form", method="POST")
|
||||||
|
button.btn.btn-primary.btn-block(type="submit") Click here to reset password
|
||||||
else
|
else
|
||||||
.alert.alert-danger.center.messagebox
|
.alert.alert-danger.center.messagebox
|
||||||
strong Password recovery failed
|
strong Password recovery failed
|
||||||
|
|
9
templates/banned_channel.pug
Normal file
9
templates/banned_channel.pug
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
.col-md-12
|
||||||
|
.alert.alert-danger
|
||||||
|
h1 Banned Channel
|
||||||
|
strong This channel is banned:
|
||||||
|
p
|
||||||
|
= externalReason
|
|
@ -46,7 +46,7 @@ html(lang="en")
|
||||||
#userlist
|
#userlist
|
||||||
#messagebuffer.linewrap
|
#messagebuffer.linewrap
|
||||||
form(action="javascript:void(0)")
|
form(action="javascript:void(0)")
|
||||||
input#chatline.form-control(type="text", maxlength="320", style="display: none")
|
input#chatline.form-control(type="text", maxlength=maxMsgLen, style="display: none")
|
||||||
#guestlogin.input-group
|
#guestlogin.input-group
|
||||||
span.input-group-addon Guest login
|
span.input-group-addon Guest login
|
||||||
input#guestname.form-control(type="text", placeholder="Name")
|
input#guestname.form-control(type="text", placeholder="Name")
|
||||||
|
@ -63,10 +63,10 @@ html(lang="en")
|
||||||
button#emotelistbtn.btn.btn-sm.btn-default Emote List
|
button#emotelistbtn.btn.btn-sm.btn-default Emote List
|
||||||
#rightcontrols.col-lg-7.col-md-7
|
#rightcontrols.col-lg-7.col-md-7
|
||||||
#plcontrol.btn-group
|
#plcontrol.btn-group
|
||||||
button#showsearch.btn.btn-sm.btn-default(title="Search for a video", data-toggle="collapse", data-target="#searchcontrol")
|
|
||||||
span.glyphicon.glyphicon-search
|
|
||||||
button#showmediaurl.btn.btn-sm.btn-default(title="Add video from URL", data-toggle="collapse", data-target="#addfromurl")
|
button#showmediaurl.btn.btn-sm.btn-default(title="Add video from URL", data-toggle="collapse", data-target="#addfromurl")
|
||||||
span.glyphicon.glyphicon-plus
|
span.glyphicon.glyphicon-plus
|
||||||
|
button#showsearch.btn.btn-sm.btn-default(title="Search for a video", data-toggle="collapse", data-target="#searchcontrol")
|
||||||
|
span.glyphicon.glyphicon-search
|
||||||
button#showcustomembed.btn.btn-sm.btn-default(title="Embed a custom frame", data-toggle="collapse", data-target="#customembed")
|
button#showcustomembed.btn.btn-sm.btn-default(title="Embed a custom frame", data-toggle="collapse", data-target="#customembed")
|
||||||
span.glyphicon.glyphicon-th-large
|
span.glyphicon.glyphicon-th-large
|
||||||
button#showplaylistmanager.btn.btn-sm.btn-default(title="Manage playlists", data-toggle="collapse", data-target="#playlistmanager")
|
button#showplaylistmanager.btn.btn-sm.btn-default(title="Manage playlists", data-toggle="collapse", data-target="#playlistmanager")
|
||||||
|
@ -249,14 +249,18 @@ html(lang="en")
|
||||||
script(src="/js/paginator.js")
|
script(src="/js/paginator.js")
|
||||||
script(src="/js/ui.js")
|
script(src="/js/ui.js")
|
||||||
script(src="/js/callbacks.js")
|
script(src="/js/callbacks.js")
|
||||||
|
script(defer, src="/js/vjs/dash.all.min.js")
|
||||||
|
script(defer, src="/js/vjs/video.js")
|
||||||
|
script(defer, src="/js/vjs/videojs-dash.js")
|
||||||
|
script(defer, src="/js/vjs/videojs-hlsjs-plugin.js")
|
||||||
|
script(defer, src="/js/vjs/videojs-resolution-switcher.js")
|
||||||
|
script(defer, src="/js/vjs/videojs-audio-switcher.js")
|
||||||
|
script(defer, src="/js/octopus/subtitles-octopus.js")
|
||||||
|
script(defer, src="/js/playerjs-0.0.12.js")
|
||||||
|
script(defer, src="/js/niconico.js")
|
||||||
|
script(defer, src="/js/peertube.js")
|
||||||
|
script(defer, src="/js/sc.js")
|
||||||
script(defer, src="https://www.youtube.com/iframe_api")
|
script(defer, src="https://www.youtube.com/iframe_api")
|
||||||
script(defer, src="https://api.dmcdn.net/all.js")
|
script(defer, src="https://api.dmcdn.net/all.js")
|
||||||
script(defer, src="https://player.vimeo.com/api/player.js")
|
script(defer, src="https://player.vimeo.com/api/player.js")
|
||||||
script(defer, src="/js/sc.js")
|
|
||||||
script(defer, src="/js/video.js")
|
|
||||||
script(defer, src="/js/videojs-contrib-hls.min.js")
|
|
||||||
script(defer, src="/js/videojs-resolution-switcher.js")
|
|
||||||
script(defer, src="/js/playerjs-0.0.12.js")
|
|
||||||
script(defer, src="/js/dash.all.min.js")
|
|
||||||
script(defer, src="/js/videojs-dash.js")
|
|
||||||
script(defer, src="https://player.twitch.tv/js/embed/v1.js")
|
script(defer, src="https://player.twitch.tv/js/embed/v1.js")
|
||||||
|
|
|
@ -3,7 +3,7 @@ mixin footer
|
||||||
.container
|
.container
|
||||||
p.text-muted.credit.
|
p.text-muted.credit.
|
||||||
Powered by CyTube, available on <a href="https://github.com/calzoneman/sync" target="_blank" rel="noreferrer noopener">GitHub</a> · <a href="/contact" target="_blank">Contact</a> · <a href="https://github.com/calzoneman/sync/wiki" target="_blank" rel="noopener noreferrer">Wiki</a>
|
Powered by CyTube, available on <a href="https://github.com/calzoneman/sync" target="_blank" rel="noreferrer noopener">GitHub</a> · <a href="/contact" target="_blank">Contact</a> · <a href="https://github.com/calzoneman/sync/wiki" target="_blank" rel="noopener noreferrer">Wiki</a>
|
||||||
script(src="/js/jquery-1.11.0.min.js")
|
script(src="/js/jquery-1.12.4.min.js")
|
||||||
// Must be included before jQuery-UI since jQuery-UI overrides jQuery.fn.button
|
// Must be included before jQuery-UI since jQuery-UI overrides jQuery.fn.button
|
||||||
// I should really abandon this crap one day
|
// I should really abandon this crap one day
|
||||||
script(src="/js/jquery-ui.js")
|
script(src="/js/jquery-ui.js")
|
||||||
|
|
|
@ -3,18 +3,21 @@ extends layout.pug
|
||||||
block content
|
block content
|
||||||
.col-md-8.col-md-offset-2
|
.col-md-8.col-md-offset-2
|
||||||
h1 Google Drive Userscript
|
h1 Google Drive Userscript
|
||||||
h2 Why?
|
h2 Disclaimer
|
||||||
p.
|
.alert.alert-danger.messagebox
|
||||||
Since Google Drive support was launched in early 2014, it has broken
|
strong Unsupported
|
||||||
at least 4-5 times, requiring increasing effort to get it working again
|
p.
|
||||||
and disrupting many channels. This is because there is no official API
|
This functionality is provided <strong>as-is</strong> for backwards
|
||||||
for it like there is for YouTube videos, which means support for it
|
compatibility for existing users for whom it already is known to work.
|
||||||
relies on undocumented tricks. In August 2016, the decision was made
|
There are many reasons, known and unknown, for which it may
|
||||||
to phase out the native support for Google Drive and instead require
|
<strong>not</strong> work for you; please note the staff in CyTube
|
||||||
users to install a userscript, which allows to bypass certain browser
|
support channels cannot provide any troubleshooting assistance and you
|
||||||
restrictions and make the code easier, simpler, and less prone to failure
|
will be asked to simply use a different video provider.
|
||||||
(it could still break due to future Google Drive changes, but is less
|
p.
|
||||||
likely to be difficult to fix).
|
This functionality was originally added so that users could share their
|
||||||
|
own personal videos stored in their Drive. No support whatsoever will
|
||||||
|
be provided to users attempting to use it to circumvent copyright
|
||||||
|
restrictions on third-party video hosts.
|
||||||
h2 How It Works
|
h2 How It Works
|
||||||
p.
|
p.
|
||||||
The userscript is a short script that you can install using a browser
|
The userscript is a short script that you can install using a browser
|
||||||
|
|
36
templates/iframe.pug
Normal file
36
templates/iframe.pug
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
doctype html
|
||||||
|
html(lang="en")
|
||||||
|
head
|
||||||
|
meta(charset="utf-8")
|
||||||
|
meta(name="viewport", content="width=device-width, initial-scale=1.0")
|
||||||
|
meta(name="referrer", content="same-origin")
|
||||||
|
link(rel="stylesheet", href="/css/video-js.css")
|
||||||
|
link(rel="stylesheet", href="/css/videojs-resolution-switcher.css")
|
||||||
|
style.
|
||||||
|
body { overflow-y: hidden }
|
||||||
|
body
|
||||||
|
#wrap
|
||||||
|
#videowrap
|
||||||
|
#ytapiplayer
|
||||||
|
script.
|
||||||
|
const USEROPTS = {
|
||||||
|
default_quality: 'auto'
|
||||||
|
}
|
||||||
|
const CLIENT = {
|
||||||
|
leader: false
|
||||||
|
}
|
||||||
|
let VOLUME = 0;
|
||||||
|
function waitUntilDefined(obj, key, fn) {
|
||||||
|
if(typeof obj[key] === "undefined") {
|
||||||
|
setTimeout(function () {
|
||||||
|
waitUntilDefined(obj, key, fn);
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
script(src="/js/jquery-1.12.4.min.js")
|
||||||
|
script(src="/js/vjs/video.js")
|
||||||
|
script(src="/js/vjs/videojs-resolution-switcher.js")
|
||||||
|
script(src="/js/playerjs-0.0.12.js")
|
||||||
|
script(src="/js/player.js")
|
|
@ -21,8 +21,15 @@ block content
|
||||||
|
|
||||||
append footer
|
append footer
|
||||||
script(type="text/javascript").
|
script(type="text/javascript").
|
||||||
$("#channelname").keydown(function (ev) {
|
const entrance = document.querySelector('#channelname');
|
||||||
|
entrance.addEventListener('keydown', function (ev) {
|
||||||
if (ev.keyCode === 13) {
|
if (ev.keyCode === 13) {
|
||||||
location.href = "/#{channelPath}/" + $("#channelname").val();
|
const channel = `/${CHANNELPATH}/${entrance.value}`;
|
||||||
|
if (ev.shiftKey || ev.ctrlKey) {
|
||||||
|
window.open(channel, '_blank');
|
||||||
|
entrance.value = '';
|
||||||
|
} else {
|
||||||
|
location.href = channel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -68,11 +68,6 @@ mixin us-playback
|
||||||
form.form-horizontal(action="javascript:void(0)")
|
form.form-horizontal(action="javascript:void(0)")
|
||||||
+rcheckbox("us-synch", "Synchronize video playback")
|
+rcheckbox("us-synch", "Synchronize video playback")
|
||||||
+textbox("us-synch-accuracy", "Synch threshold (seconds)", "2")
|
+textbox("us-synch-accuracy", "Synch threshold (seconds)", "2")
|
||||||
+rcheckbox("us-wmode-transparent", "Set wmode=transparent")
|
|
||||||
.form-group
|
|
||||||
.col-sm-4
|
|
||||||
.col-sm-8
|
|
||||||
p.text-info Setting <code>wmode=transparent</code> allows objects to be displayed above the video player, but may cause performance issues on some systems.
|
|
||||||
+rcheckbox("us-hidevideo", "Remove the video player")
|
+rcheckbox("us-hidevideo", "Remove the video player")
|
||||||
+rcheckbox("us-playlistbuttons", "Hide playlist buttons by default")
|
+rcheckbox("us-playlistbuttons", "Hide playlist buttons by default")
|
||||||
+rcheckbox("us-oldbtns", "Old style playlist buttons")
|
+rcheckbox("us-oldbtns", "Old style playlist buttons")
|
||||||
|
@ -91,6 +86,7 @@ mixin us-playback
|
||||||
.col-sm-4
|
.col-sm-4
|
||||||
.col-sm-8
|
.col-sm-8
|
||||||
p.text-info Due to technical changes on YouTube's side, the CyTube quality preference can no longer be automatically applied on YouTube videos. See <a href="https://github.com/calzoneman/sync/issues/726" rel="noopener noreferer" target="_blank">this GitHub issue</a> for details.
|
p.text-info Due to technical changes on YouTube's side, the CyTube quality preference can no longer be automatically applied on YouTube videos. See <a href="https://github.com/calzoneman/sync/issues/726" rel="noopener noreferer" target="_blank">this GitHub issue</a> for details.
|
||||||
|
+rcheckbox("us-peertube", "Accept PeerTube embeds automatically")
|
||||||
|
|
||||||
mixin us-chat
|
mixin us-chat
|
||||||
#us-chat.tab-pane
|
#us-chat.tab-pane
|
||||||
|
|
|
@ -97,7 +97,10 @@ describe('PollModule', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let pollModule = new PollModule(fakeChannel);
|
let pollModule;
|
||||||
|
beforeEach(() => {
|
||||||
|
pollModule = new PollModule(fakeChannel);
|
||||||
|
});
|
||||||
|
|
||||||
it('creates a valid poll', () => {
|
it('creates a valid poll', () => {
|
||||||
let sentNewPoll = false;
|
let sentNewPoll = false;
|
||||||
|
@ -122,10 +125,54 @@ describe('PollModule', () => {
|
||||||
}, (ackResult) => {
|
}, (ackResult) => {
|
||||||
assert(!ackResult.error, `Unexpected error: ${ackResult.error}`);
|
assert(!ackResult.error, `Unexpected error: ${ackResult.error}`);
|
||||||
});
|
});
|
||||||
assert(sentClosePoll, 'Expected broadcast of closePoll event');
|
assert(!sentClosePoll, 'Unexpected broadcast of closePoll event');
|
||||||
assert(sentNewPoll, 'Expected broadcast of newPoll event');
|
assert(sentNewPoll, 'Expected broadcast of newPoll event');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('closes an existing poll when a new one is created', () => {
|
||||||
|
let sentNewPoll = 0;
|
||||||
|
let sentClosePoll = 0;
|
||||||
|
let sentUpdatePoll = 0;
|
||||||
|
fakeChannel.broadcastToRoom = (event, data, room) => {
|
||||||
|
if (room === 'testChannel:viewHidden' && event === 'newPoll') {
|
||||||
|
sentNewPoll++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fakeChannel.broadcastAll = (event, data) => {
|
||||||
|
if (event === 'closePoll') {
|
||||||
|
sentClosePoll++;
|
||||||
|
} else if (event === 'updatePoll') {
|
||||||
|
sentUpdatePoll++;
|
||||||
|
assert.deepStrictEqual(data.counts, [0, 0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pollModule.handleNewPoll(fakeUser, {
|
||||||
|
title: 'test poll',
|
||||||
|
opts: [
|
||||||
|
'option 1',
|
||||||
|
'option 2'
|
||||||
|
],
|
||||||
|
obscured: true
|
||||||
|
}, (ackResult) => {
|
||||||
|
assert(!ackResult.error, `Unexpected error: ${ackResult.error}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
pollModule.handleNewPoll(fakeUser, {
|
||||||
|
title: 'poll 2',
|
||||||
|
opts: [
|
||||||
|
'option 3',
|
||||||
|
'option 4'
|
||||||
|
],
|
||||||
|
obscured: false
|
||||||
|
}, (ackResult) => {
|
||||||
|
assert(!ackResult.error, `Unexpected error: ${ackResult.error}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(sentClosePoll, 1, 'Expected 1 broadcast of closePoll event');
|
||||||
|
assert.strictEqual(sentUpdatePoll, 1, 'Expected 1 broadcast of updatePoll event');
|
||||||
|
assert.strictEqual(sentNewPoll, 2, 'Expected 2 broadcasts of newPoll event');
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects an invalid poll', () => {
|
it('rejects an invalid poll', () => {
|
||||||
fakeChannel.broadcastToRoom = (event, data, room) => {
|
fakeChannel.broadcastToRoom = (event, data, room) => {
|
||||||
assert(false, 'Expected no events to be sent');
|
assert(false, 'Expected no events to be sent');
|
||||||
|
@ -171,4 +218,4 @@ describe('PollModule', () => {
|
||||||
assert(sentErrorMsg, 'Expected to send errorMsg since ack was missing');
|
assert(sentErrorMsg, 'Expected to send errorMsg since ack was missing');
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -77,7 +77,9 @@ describe('VoteskipModule', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
voteskipModule.poll = {
|
voteskipModule.poll = {
|
||||||
counts: [1]
|
toUpdateFrame() {
|
||||||
|
return { counts: [1] };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
voteskipModule.update();
|
voteskipModule.update();
|
||||||
assert.equal(voteskipModule.poll, false, 'Expected voteskip poll to be reset to false');
|
assert.equal(voteskipModule.poll, false, 'Expected voteskip poll to be reset to false');
|
||||||
|
@ -93,11 +95,30 @@ describe('VoteskipModule', () => {
|
||||||
sentMessage = true;
|
sentMessage = true;
|
||||||
};
|
};
|
||||||
voteskipModule.poll = {
|
voteskipModule.poll = {
|
||||||
counts: [1]
|
toUpdateFrame() {
|
||||||
|
return { counts: [1] };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
voteskipModule.update();
|
voteskipModule.update();
|
||||||
assert(sentMessage, 'Expected voteskip passed message');
|
assert(sentMessage, 'Expected voteskip passed message');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('requires at least one vote to pass', () => {
|
||||||
|
let sentMessage = false;
|
||||||
|
fakeChannel.broadcastAll = (frame, data) => {
|
||||||
|
assert.strictEqual(frame, 'chatMsg');
|
||||||
|
assert(/voteskip passed/i.test(data.msg), 'Expected voteskip passed message')
|
||||||
|
sentMessage = true;
|
||||||
|
};
|
||||||
|
fakeUser.is = flag => (flag == Flags.U_AFK);
|
||||||
|
voteskipModule.poll = {
|
||||||
|
toUpdateFrame() {
|
||||||
|
return { counts: [0] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
voteskipModule.update();
|
||||||
|
assert(!sentMessage, 'Expected voteskip not to pass');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#calcUsercounts', () => {
|
describe('#calcUsercounts', () => {
|
||||||
|
|
|
@ -90,15 +90,6 @@ describe('custom-media', () => {
|
||||||
assert.throws(() => validate(invalid), /URL protocol must be HTTPS/);
|
assert.throws(() => validate(invalid), /URL protocol must be HTTPS/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects non-live DASH', () => {
|
|
||||||
invalid.live = false;
|
|
||||||
invalid.sources[0].contentType = 'application/dash+xml';
|
|
||||||
|
|
||||||
assert.throws(
|
|
||||||
() => validate(invalid),
|
|
||||||
/contentType "application\/dash\+xml" requires live: true/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#validateSources', () => {
|
describe('#validateSources', () => {
|
||||||
|
@ -242,7 +233,8 @@ describe('custom-media', () => {
|
||||||
contentType: 'text/vtt',
|
contentType: 'text/vtt',
|
||||||
name: 'English Subtitles'
|
name: 'English Subtitles'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
thumbnail: 'https://example.com/thumb.jpg',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -330,7 +322,8 @@ describe('custom-media', () => {
|
||||||
contentType: 'text/vtt',
|
contentType: 'text/vtt',
|
||||||
name: 'English Subtitles'
|
name: 'English Subtitles'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
thumbnail: 'https://example.com/thumb.jpg',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
262
test/poll.js
Normal file
262
test/poll.js
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
const assert = require('assert');
|
||||||
|
const { Poll } = require('../lib/poll');
|
||||||
|
|
||||||
|
describe('Poll', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('constructs a poll', () => {
|
||||||
|
let poll = Poll.create(
|
||||||
|
'pollster',
|
||||||
|
'Which is better?',
|
||||||
|
[
|
||||||
|
'Coke',
|
||||||
|
'Pepsi'
|
||||||
|
]
|
||||||
|
/* default opts */
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(poll.createdBy, 'pollster');
|
||||||
|
assert.strictEqual(poll.title, 'Which is better?');
|
||||||
|
assert.deepStrictEqual(poll.choices, ['Coke', 'Pepsi']);
|
||||||
|
assert.strictEqual(poll.hideVotes, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('constructs a poll with hidden vote setting', () => {
|
||||||
|
let poll = Poll.create(
|
||||||
|
'pollster',
|
||||||
|
'Which is better?',
|
||||||
|
[
|
||||||
|
'Coke',
|
||||||
|
'Pepsi'
|
||||||
|
],
|
||||||
|
{ hideVotes: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(poll.hideVotes, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sanitizes title and choices', () => {
|
||||||
|
let poll = Poll.create(
|
||||||
|
'pollster',
|
||||||
|
'Which is better? <script></script>',
|
||||||
|
[
|
||||||
|
'<strong>Coke</strong>',
|
||||||
|
'Pepsi'
|
||||||
|
]
|
||||||
|
/* default opts */
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(poll.title, 'Which is better? <script></script>');
|
||||||
|
assert.deepStrictEqual(poll.choices, ['<strong>Coke</strong>', 'Pepsi']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces URLs in title and choices', () => {
|
||||||
|
let poll = Poll.create(
|
||||||
|
'pollster',
|
||||||
|
'Which is better? https://example.com',
|
||||||
|
[
|
||||||
|
'Coke https://example.com',
|
||||||
|
'Pepsi'
|
||||||
|
]
|
||||||
|
/* default opts */
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
poll.title,
|
||||||
|
'Which is better? <a href="https://example.com" target="_blank" rel="noopener noreferer">https://example.com</a>'
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
poll.choices,
|
||||||
|
[
|
||||||
|
'Coke <a href="https://example.com" target="_blank" rel="noopener noreferer">https://example.com</a>',
|
||||||
|
'Pepsi'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#countVote', () => {
|
||||||
|
let poll;
|
||||||
|
beforeEach(() => {
|
||||||
|
poll = Poll.create(
|
||||||
|
'pollster',
|
||||||
|
'Which is better?',
|
||||||
|
[
|
||||||
|
'Coke',
|
||||||
|
'Pepsi'
|
||||||
|
]
|
||||||
|
/* default opts */
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts a new vote', () => {
|
||||||
|
assert.strictEqual(poll.countVote('userA', 0), true);
|
||||||
|
assert.strictEqual(poll.countVote('userB', 1), true);
|
||||||
|
assert.strictEqual(poll.countVote('userC', 0), true);
|
||||||
|
|
||||||
|
let { counts } = poll.toUpdateFrame();
|
||||||
|
assert.deepStrictEqual(counts, [2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not count a revote for the same choice', () => {
|
||||||
|
assert.strictEqual(poll.countVote('userA', 0), true);
|
||||||
|
assert.strictEqual(poll.countVote('userA', 0), false);
|
||||||
|
|
||||||
|
let { counts } = poll.toUpdateFrame();
|
||||||
|
assert.deepStrictEqual(counts, [1, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes a vote to a different choice', () => {
|
||||||
|
assert.strictEqual(poll.countVote('userA', 0), true);
|
||||||
|
assert.strictEqual(poll.countVote('userA', 1), true);
|
||||||
|
|
||||||
|
let { counts } = poll.toUpdateFrame();
|
||||||
|
assert.deepStrictEqual(counts, [0, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores out of range votes', () => {
|
||||||
|
assert.strictEqual(poll.countVote('userA', 1000), false);
|
||||||
|
assert.strictEqual(poll.countVote('userA', -10), false);
|
||||||
|
|
||||||
|
let { counts } = poll.toUpdateFrame();
|
||||||
|
assert.deepStrictEqual(counts, [0, 0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#uncountVote', () => {
|
||||||
|
let poll;
|
||||||
|
beforeEach(() => {
|
||||||
|
poll = Poll.create(
|
||||||
|
'pollster',
|
||||||
|
'Which is better?',
|
||||||
|
[
|
||||||
|
'Coke',
|
||||||
|
'Pepsi'
|
||||||
|
]
|
||||||
|
/* default opts */
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uncounts an existing vote', () => {
|
||||||
|
assert.strictEqual(poll.countVote('userA', 0), true);
|
||||||
|
assert.strictEqual(poll.uncountVote('userA', 0), true);
|
||||||
|
|
||||||
|
let { counts } = poll.toUpdateFrame();
|
||||||
|
assert.deepStrictEqual(counts, [0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not uncount if there is no existing vote', () => {
|
||||||
|
assert.strictEqual(poll.uncountVote('userA', 0), false);
|
||||||
|
|
||||||
|
let { counts } = poll.toUpdateFrame();
|
||||||
|
assert.deepStrictEqual(counts, [0, 0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#toUpdateFrame', () => {
|
||||||
|
let poll;
|
||||||
|
beforeEach(() => {
|
||||||
|
poll = Poll.create(
|
||||||
|
'pollster',
|
||||||
|
'Which is better?',
|
||||||
|
[
|
||||||
|
'Coke',
|
||||||
|
'Pepsi'
|
||||||
|
]
|
||||||
|
/* default opts */
|
||||||
|
);
|
||||||
|
poll.countVote('userA', 0);
|
||||||
|
poll.countVote('userB', 1);
|
||||||
|
poll.countVote('userC', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates an update frame', () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
poll.toUpdateFrame(),
|
||||||
|
{
|
||||||
|
title: 'Which is better?',
|
||||||
|
options: ['Coke', 'Pepsi'],
|
||||||
|
counts: [2, 1],
|
||||||
|
initiator: 'pollster',
|
||||||
|
timestamp: poll.createdAt.getTime()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides votes when poll is hidden', () => {
|
||||||
|
poll.hideVotes = true;
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
poll.toUpdateFrame(),
|
||||||
|
{
|
||||||
|
title: 'Which is better?',
|
||||||
|
options: ['Coke', 'Pepsi'],
|
||||||
|
counts: ['?', '?'],
|
||||||
|
initiator: 'pollster',
|
||||||
|
timestamp: poll.createdAt.getTime()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays hidden votes when requested', () => {
|
||||||
|
poll.hideVotes = true;
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
poll.toUpdateFrame(true),
|
||||||
|
{
|
||||||
|
title: 'Which is better?',
|
||||||
|
options: ['Coke', 'Pepsi'],
|
||||||
|
counts: ['2?', '1?'],
|
||||||
|
initiator: 'pollster',
|
||||||
|
timestamp: poll.createdAt.getTime()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#toChannelData/fromChannelData', () => {
|
||||||
|
it('round trips a poll', () => {
|
||||||
|
let data = {
|
||||||
|
title: '<strong>ready?</strong>',
|
||||||
|
initiator: 'aUser',
|
||||||
|
options: ['yes', 'no'],
|
||||||
|
counts: [0, 1],
|
||||||
|
votes:{
|
||||||
|
'1.2.3.4': null, // Previous poll code would set removed votes to null
|
||||||
|
'5.6.7.8': 1
|
||||||
|
},
|
||||||
|
obscured: false,
|
||||||
|
timestamp: 1483414981110
|
||||||
|
};
|
||||||
|
|
||||||
|
let poll = Poll.fromChannelData(data);
|
||||||
|
|
||||||
|
// New code does not store null votes
|
||||||
|
data.votes = { '5.6.7.8': 1 };
|
||||||
|
data.retainVotes = false;
|
||||||
|
assert.deepStrictEqual(poll.toChannelData(), data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('coerces a missing timestamp to the current time', () => {
|
||||||
|
let data = {
|
||||||
|
title: '<strong>ready?</strong>',
|
||||||
|
initiator: 'aUser',
|
||||||
|
options: ['yes', 'no'],
|
||||||
|
counts: [0, 1],
|
||||||
|
votes:{
|
||||||
|
'1.2.3.4': null,
|
||||||
|
'5.6.7.8': 1
|
||||||
|
},
|
||||||
|
obscured: false
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Date.now();
|
||||||
|
let poll = Poll.fromChannelData(data);
|
||||||
|
const { timestamp } = poll.toChannelData();
|
||||||
|
if (typeof timestamp !== 'number' || isNaN(timestamp))
|
||||||
|
assert.fail(`Unexpected timestamp: ${timestamp}`);
|
||||||
|
|
||||||
|
if (Math.abs(timestamp - now) > 1000)
|
||||||
|
assert.fail(`Unexpected timestamp: ${timestamp}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
52
test/util/simple-cache.js
Normal file
52
test/util/simple-cache.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
const { SimpleCache } = require('../../lib/util/simple-cache');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
describe('SimpleCache', () => {
|
||||||
|
const CACHE_MAX_ELEM = 5;
|
||||||
|
const CACHE_MAX_AGE = 5;
|
||||||
|
let cache;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cache = new SimpleCache({
|
||||||
|
maxElem: CACHE_MAX_ELEM,
|
||||||
|
maxAge: CACHE_MAX_AGE
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets, gets, and deletes a value', () => {
|
||||||
|
assert.strictEqual(cache.get('foo'), null);
|
||||||
|
|
||||||
|
cache.put('foo', 'bar');
|
||||||
|
assert.strictEqual(cache.get('foo'), 'bar');
|
||||||
|
|
||||||
|
cache.delete('foo');
|
||||||
|
assert.strictEqual(cache.get('foo'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return an expired value', done => {
|
||||||
|
cache.put('foo', 'bar');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
assert.strictEqual(cache.get('foo'), null);
|
||||||
|
done();
|
||||||
|
}, CACHE_MAX_AGE + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleans up old values', done => {
|
||||||
|
cache.put('foo', 'bar');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
assert.strictEqual(cache.get('foo'), null);
|
||||||
|
done();
|
||||||
|
}, CACHE_MAX_AGE * 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the oldest entry if max elem is reached', () => {
|
||||||
|
for (let i = 0; i < CACHE_MAX_ELEM + 1; i++) {
|
||||||
|
cache.put(`foo${i}`, 'bar');
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(cache.get('foo0'), null);
|
||||||
|
assert.strictEqual(cache.get('foo1'), 'bar');
|
||||||
|
});
|
||||||
|
});
|
59
www/.eslintrc.json
Normal file
59
www/.eslintrc.json
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"env": { "browser": true, "jquery": true },
|
||||||
|
"globals": {
|
||||||
|
"CHANNEL": "writable",
|
||||||
|
"CHANNELNAME": "writable",
|
||||||
|
"CHATHIST": "writable",
|
||||||
|
"CHATHISTIDX": "writable",
|
||||||
|
"CHATSOUND": "writable",
|
||||||
|
"CHATTHROTTLE": "writable",
|
||||||
|
"CLIENT": "writable",
|
||||||
|
"CSEMOTELIST": "writable",
|
||||||
|
"DEFAULT_THEME": "writable",
|
||||||
|
"EMOTELIST": "writable",
|
||||||
|
"EMOTELISTMODAL": "writable",
|
||||||
|
"FILTER_FROM": "writable",
|
||||||
|
"FILTER_TO": "writable",
|
||||||
|
"FOCUSED": "writable",
|
||||||
|
"GS_VERSION": "writable",
|
||||||
|
"HAS_CONNECTED_BEFORE": "writable",
|
||||||
|
"IGNORE_SCROLL_EVENT": "writable",
|
||||||
|
"IGNORED": "writable",
|
||||||
|
"IMAGE_MATCH": "writable",
|
||||||
|
"JSPREF": "writable",
|
||||||
|
"KICKED": "writable",
|
||||||
|
"LASTCHAT": "writable",
|
||||||
|
"LEADTMR": "writable",
|
||||||
|
"PAGETITLE": "writable",
|
||||||
|
"PL_ACTION_QUEUE": "writable",
|
||||||
|
"PL_AFTER": "writable",
|
||||||
|
"PL_CURRENT": "writable",
|
||||||
|
"PL_FROM": "writable",
|
||||||
|
"PL_QUEUED_ACTIONS": "writable",
|
||||||
|
"PL_WAIT_SCROLL": "writable",
|
||||||
|
"PLAYER": "writable",
|
||||||
|
"REBUILDING": "writable",
|
||||||
|
"SCROLLCHAT": "writable",
|
||||||
|
"SOCKETIO_CONNECT_ERROR_COUNT": "writable",
|
||||||
|
"SUPERADMIN": "writable",
|
||||||
|
"TITLE_BLINK": "writable",
|
||||||
|
"USEROPTS": "writable",
|
||||||
|
"VHEIGHT": "writable",
|
||||||
|
"VOLUME": "writable",
|
||||||
|
"VWIDTH": "writable",
|
||||||
|
"CyTube": "writable",
|
||||||
|
"Rank": "writable",
|
||||||
|
"getOpt": "writable",
|
||||||
|
"setOpt": "writable",
|
||||||
|
"socket": "writable"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"no-jquery"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:no-jquery/deprecated"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-jquery/no-event-shorthand": "error"
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
|
@ -28,6 +28,11 @@
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#usercount {
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 2;
|
||||||
|
}
|
||||||
|
|
||||||
#userlist {
|
#userlist {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -90,6 +95,12 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#chatheader {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
#chatheader > p, #videowrap-header {
|
#chatheader > p, #videowrap-header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -582,7 +593,7 @@ table td {
|
||||||
}
|
}
|
||||||
|
|
||||||
#userlisttoggle {
|
#userlisttoggle {
|
||||||
padding-top: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue_entry {
|
.queue_entry {
|
||||||
|
@ -651,6 +662,7 @@ input#logout[type="submit"]:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
#newmessages-indicator {
|
#newmessages-indicator {
|
||||||
|
position: relative;
|
||||||
margin-top: -30px;
|
margin-top: -30px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|
1658
www/css/video-js.css
1658
www/css/video-js.css
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
||||||
.vjs-resolution-button .vjs-menu-icon:before {
|
.vjs-resolution-button .vjs-icon-placeholder:before {
|
||||||
content: '\f110';
|
content: '\f110';
|
||||||
font-family: VideoJS;
|
font-family: VideoJS;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
var chosenServer = IO_SERVERS[0]; // Is the array even necessary for the ACP?
|
var chosenServer = IO_SERVERS[0]; // Is the array even necessary for the ACP?
|
||||||
|
|
||||||
var opts = {
|
var opts = {
|
||||||
secure: chosenServer.secure
|
secure: chosenServer.secure,
|
||||||
|
withCredentials: true // needed for sio cookie to work
|
||||||
};
|
};
|
||||||
|
|
||||||
window.socket = io.connect(chosenServer.url, opts);
|
window.socket = io.connect(chosenServer.url, opts);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
Callbacks = {
|
const Callbacks = {
|
||||||
/* fired when socket connection completes */
|
/* fired when socket connection completes */
|
||||||
connect: function() {
|
connect: function() {
|
||||||
HAS_CONNECTED_BEFORE = true;
|
HAS_CONNECTED_BEFORE = true;
|
||||||
|
@ -82,7 +82,7 @@ Callbacks = {
|
||||||
var announcement = makeAlert(data.title, data.text + signature)
|
var announcement = makeAlert(data.title, data.text + signature)
|
||||||
.appendTo($("#announcements"));
|
.appendTo($("#announcements"));
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
announcement.find(".close").click(function suppressThisAnnouncement() {
|
announcement.find(".close").on('click', function suppressThisAnnouncement() {
|
||||||
CyTube.ui.suppressedAnnouncementId = data.id;
|
CyTube.ui.suppressedAnnouncementId = data.id;
|
||||||
setOpt("suppressed_announcement_id", data.id);
|
setOpt("suppressed_announcement_id", data.id);
|
||||||
});
|
});
|
||||||
|
@ -179,7 +179,7 @@ Callbacks = {
|
||||||
|
|
||||||
$("<button/>").addClass("close pull-right")
|
$("<button/>").addClass("close pull-right")
|
||||||
.appendTo(div)
|
.appendTo(div)
|
||||||
.click(function () {
|
.on('click', function () {
|
||||||
div.parent().remove();
|
div.parent().remove();
|
||||||
})
|
})
|
||||||
.html("×");
|
.html("×");
|
||||||
|
@ -444,7 +444,7 @@ Callbacks = {
|
||||||
var li = $("<li/>").appendTo(menu);
|
var li = $("<li/>").appendTo(menu);
|
||||||
$("<a/>").attr("href", "javascript:void(0)")
|
$("<a/>").attr("href", "javascript:void(0)")
|
||||||
.html(disp)
|
.html(disp)
|
||||||
.click(function() {
|
.on('click', function() {
|
||||||
socket.emit("borrow-rank", r);
|
socket.emit("borrow-rank", r);
|
||||||
})
|
})
|
||||||
.appendTo(li);
|
.appendTo(li);
|
||||||
|
@ -658,8 +658,7 @@ Callbacks = {
|
||||||
}
|
}
|
||||||
$("#drinkcount").text(text);
|
$("#drinkcount").text(text);
|
||||||
$("#drinkbar").show();
|
$("#drinkbar").show();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$("#drinkbar").hide();
|
$("#drinkbar").hide();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -752,8 +751,7 @@ Callbacks = {
|
||||||
if(data.temp) {
|
if(data.temp) {
|
||||||
btn.html(btn.html().replace("Make Temporary",
|
btn.html(btn.html().replace("Make Temporary",
|
||||||
"Make Permanent"));
|
"Make Permanent"));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
btn.html(btn.html().replace("Make Permanent",
|
btn.html(btn.html().replace("Make Permanent",
|
||||||
"Make Temporary"));
|
"Make Temporary"));
|
||||||
}
|
}
|
||||||
|
@ -867,8 +865,7 @@ Callbacks = {
|
||||||
$("#qlockbtn").find("span")
|
$("#qlockbtn").find("span")
|
||||||
.removeClass("glyphicon-lock")
|
.removeClass("glyphicon-lock")
|
||||||
.addClass("glyphicon-ok");
|
.addClass("glyphicon-ok");
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$("#qlockbtn").removeClass("btn-success")
|
$("#qlockbtn").removeClass("btn-success")
|
||||||
.addClass("btn-danger")
|
.addClass("btn-danger")
|
||||||
.attr("title", "Playlist Locked");
|
.attr("title", "Playlist Locked");
|
||||||
|
@ -886,7 +883,7 @@ Callbacks = {
|
||||||
.css("margin-left", "0")
|
.css("margin-left", "0")
|
||||||
.attr("id", "search_clear")
|
.attr("id", "search_clear")
|
||||||
.text("Clear Results")
|
.text("Clear Results")
|
||||||
.click(function() {
|
.on('click', function() {
|
||||||
clearSearchResults();
|
clearSearchResults();
|
||||||
})
|
})
|
||||||
.insertBefore($("#library"));
|
.insertBefore($("#library"));
|
||||||
|
@ -927,12 +924,12 @@ Callbacks = {
|
||||||
var poll = $("<div/>").addClass("well active").prependTo($("#pollwrap"));
|
var poll = $("<div/>").addClass("well active").prependTo($("#pollwrap"));
|
||||||
$("<button/>").addClass("close pull-right").html("×")
|
$("<button/>").addClass("close pull-right").html("×")
|
||||||
.appendTo(poll)
|
.appendTo(poll)
|
||||||
.click(function() { poll.remove(); });
|
.on('click', function() { poll.remove(); });
|
||||||
if(hasPermission("pollctl")) {
|
if(hasPermission("pollctl")) {
|
||||||
$("<button/>").addClass("btn btn-danger btn-sm pull-right").text("End Poll")
|
$("<button/>").addClass("btn btn-danger btn-sm pull-right").text("End Poll")
|
||||||
.appendTo(poll)
|
.appendTo(poll)
|
||||||
.click(function() {
|
.on('click', function() {
|
||||||
socket.emit("closePoll")
|
socket.emit("closePoll");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -944,14 +941,16 @@ Callbacks = {
|
||||||
option: i
|
option: i
|
||||||
});
|
});
|
||||||
poll.find(".option button").each(function() {
|
poll.find(".option button").each(function() {
|
||||||
$(this).attr("disabled", "disabled");
|
$(this).removeClass("active");
|
||||||
|
$(this).parent().removeClass("option-selected");
|
||||||
});
|
});
|
||||||
|
$(this).addClass("active");
|
||||||
$(this).parent().addClass("option-selected");
|
$(this).parent().addClass("option-selected");
|
||||||
}
|
};
|
||||||
$("<button/>").addClass("btn btn-default btn-sm").text(data.counts[i])
|
$("<button/>").addClass("btn btn-default btn-sm").text(data.counts[i])
|
||||||
.prependTo($("<div/>").addClass("option").html(data.options[i])
|
.prependTo($("<div/>").addClass("option").html(data.options[i])
|
||||||
.appendTo(poll))
|
.appendTo(poll))
|
||||||
.click(callback);
|
.on('click', callback);
|
||||||
})(i);
|
})(i);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -979,7 +978,7 @@ Callbacks = {
|
||||||
$(this).attr("disabled", true);
|
$(this).attr("disabled", true);
|
||||||
});
|
});
|
||||||
poll.find(".btn-danger").each(function() {
|
poll.find(".btn-danger").each(function() {
|
||||||
$(this).remove()
|
$(this).remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -998,14 +997,14 @@ Callbacks = {
|
||||||
updateEmote: function (data) {
|
updateEmote: function (data) {
|
||||||
data.regex = new RegExp(data.source, "gi");
|
data.regex = new RegExp(data.source, "gi");
|
||||||
var found = false;
|
var found = false;
|
||||||
for (var i = 0; i < CHANNEL.emotes.length; i++) {
|
for (let i = 0; i < CHANNEL.emotes.length; i++) {
|
||||||
if (CHANNEL.emotes[i].name === data.name) {
|
if (CHANNEL.emotes[i].name === data.name) {
|
||||||
found = true;
|
found = true;
|
||||||
CHANNEL.emotes[i] = data;
|
CHANNEL.emotes[i] = data;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (var i = 0; i < CHANNEL.badEmotes.length; i++) {
|
for (let i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||||
if (CHANNEL.badEmotes[i].name === data.name) {
|
if (CHANNEL.badEmotes[i].name === data.name) {
|
||||||
CHANNEL.badEmotes[i] = data;
|
CHANNEL.badEmotes[i] = data;
|
||||||
break;
|
break;
|
||||||
|
@ -1048,22 +1047,20 @@ Callbacks = {
|
||||||
if(!badBefore){
|
if(!badBefore){
|
||||||
CHANNEL.badEmotes.push(data);
|
CHANNEL.badEmotes.push(data);
|
||||||
delete CHANNEL.emoteMap[oldName];
|
delete CHANNEL.emoteMap[oldName];
|
||||||
}
|
|
||||||
// Was bad before too: Update
|
// Was bad before too: Update
|
||||||
else {
|
} else {
|
||||||
for (var i = 0; i < CHANNEL.badEmotes.length; i++) {
|
for (let i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||||
if (CHANNEL.badEmotes[i].name === oldName) {
|
if (CHANNEL.badEmotes[i].name === oldName) {
|
||||||
CHANNEL.badEmotes[i] = data;
|
CHANNEL.badEmotes[i] = data;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Not bad now
|
// Not bad now
|
||||||
else {
|
} else {
|
||||||
// But was bad before: Drop from list
|
// But was bad before: Drop from list
|
||||||
if(badBefore){
|
if(badBefore){
|
||||||
for (var i = 0; i < CHANNEL.badEmotes.length; i++) {
|
for (let i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||||
if (CHANNEL.badEmotes[i].name === oldName) {
|
if (CHANNEL.badEmotes[i].name === oldName) {
|
||||||
CHANNEL.badEmotes.splice(i, 1);
|
CHANNEL.badEmotes.splice(i, 1);
|
||||||
break;
|
break;
|
||||||
|
@ -1081,7 +1078,7 @@ Callbacks = {
|
||||||
|
|
||||||
removeEmote: function (data) {
|
removeEmote: function (data) {
|
||||||
var found = -1;
|
var found = -1;
|
||||||
for (var i = 0; i < CHANNEL.emotes.length; i++) {
|
for (let i = 0; i < CHANNEL.emotes.length; i++) {
|
||||||
if (CHANNEL.emotes[i].name === data.name) {
|
if (CHANNEL.emotes[i].name === data.name) {
|
||||||
found = i;
|
found = i;
|
||||||
break;
|
break;
|
||||||
|
@ -1091,9 +1088,9 @@ Callbacks = {
|
||||||
if (found !== -1) {
|
if (found !== -1) {
|
||||||
var row = $("code:contains('" + data.name + "')").parent().parent();
|
var row = $("code:contains('" + data.name + "')").parent().parent();
|
||||||
row.hide("fade", row.remove.bind(row));
|
row.hide("fade", row.remove.bind(row));
|
||||||
CHANNEL.emotes.splice(i, 1);
|
CHANNEL.emotes.splice(found, 1);
|
||||||
delete CHANNEL.emoteMap[data.name];
|
delete CHANNEL.emoteMap[data.name];
|
||||||
for (var i = 0; i < CHANNEL.badEmotes.length; i++) {
|
for (let i = 0; i < CHANNEL.badEmotes.length; i++) {
|
||||||
if (CHANNEL.badEmotes[i].name === data.name) {
|
if (CHANNEL.badEmotes[i].name === data.name) {
|
||||||
CHANNEL.badEmotes.splice(i, 1);
|
CHANNEL.badEmotes.splice(i, 1);
|
||||||
break;
|
break;
|
||||||
|
@ -1169,20 +1166,31 @@ Callbacks = {
|
||||||
$("#voteskip").attr("disabled", false);
|
$("#voteskip").attr("disabled", false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
var SOCKET_DEBUG = localStorage.getItem('cytube_socket_debug') === 'true';
|
window.Callbacks = Callbacks;
|
||||||
setupCallbacks = function() {
|
|
||||||
|
// For sanity, do this
|
||||||
|
// localStorage.setItem('cytube_socket_omissions', '["mediaUpdate"]')
|
||||||
|
var SOCKET_DEBUG = {
|
||||||
|
enabled: (localStorage.getItem('cytube_socket_debug') === 'true'),
|
||||||
|
omit: (((data)=>{
|
||||||
|
const frames = data === null ? [] : JSON.parse(data);
|
||||||
|
return frames;
|
||||||
|
})(localStorage.getItem('cytube_socket_omissions')))
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupCallbacks() {
|
||||||
for(var key in Callbacks) {
|
for(var key in Callbacks) {
|
||||||
(function(key) {
|
(function(key) {
|
||||||
socket.on(key, function(data) {
|
socket.on(key, function(data) {
|
||||||
if (SOCKET_DEBUG) {
|
if (SOCKET_DEBUG.enabled && !SOCKET_DEBUG.omit.includes(key)) {
|
||||||
console.log(key, data);
|
console.log(key, data);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Callbacks[key](data);
|
Callbacks[key](data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (SOCKET_DEBUG) {
|
if (SOCKET_DEBUG.enabled) {
|
||||||
console.log("EXCEPTION: " + e + "\n" + e.stack);
|
console.log("EXCEPTION: " + e + "\n" + e.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1209,7 +1217,7 @@ setupCallbacks = function() {
|
||||||
.appendTo($("#announcements"));
|
.appendTo($("#announcements"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
function ioServerConnect(socketConfig) {
|
function ioServerConnect(socketConfig) {
|
||||||
if (socketConfig.error) {
|
if (socketConfig.error) {
|
||||||
|
@ -1249,7 +1257,8 @@ function ioServerConnect(socketConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var opts = {
|
var opts = {
|
||||||
secure: chosenServer.secure
|
secure: chosenServer.secure,
|
||||||
|
withCredentials: true // enable cookies for auth
|
||||||
};
|
};
|
||||||
|
|
||||||
window.socket = io(chosenServer.url, opts);
|
window.socket = io(chosenServer.url, opts);
|
||||||
|
@ -1290,7 +1299,7 @@ function initSocketIO(socketConfig) {
|
||||||
|
|
||||||
function checkLetsEncrypt(socketConfig, nonLetsEncryptError) {
|
function checkLetsEncrypt(socketConfig, nonLetsEncryptError) {
|
||||||
var servers = socketConfig.servers.filter(function (server) {
|
var servers = socketConfig.servers.filter(function (server) {
|
||||||
return !server.secure && !server.ipv6Only
|
return !server.secure && !server.ipv6Only;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (servers.length === 0) {
|
if (servers.length === 0) {
|
||||||
|
|
30
www/js/dash.all.min.js
vendored
30
www/js/dash.all.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,3 +1,55 @@
|
||||||
|
/*eslint no-unused-vars: "off"*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
/**
|
||||||
|
* Test whether the browser supports nullish-coalescing operator.
|
||||||
|
*
|
||||||
|
* Users with old browsers will probably fail to load the client correctly
|
||||||
|
* because parsing this operator in older browsers results in a SyntaxError
|
||||||
|
* that aborts compilation of the entire script (not just an exception where
|
||||||
|
* it is used). In particular, as of 2023-01-28, Utherverse ships with
|
||||||
|
* a rather old browser version (Chrome 76) and several users have reported
|
||||||
|
* it not working.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
new Function('x?.y');
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === 'SyntaxError') {
|
||||||
|
/**
|
||||||
|
* If we're at this point, we can't be sure what scripts have
|
||||||
|
* actually loaded, so construct the error alert the old
|
||||||
|
* fashioned way.
|
||||||
|
*/
|
||||||
|
var wrap = document.createElement('div');
|
||||||
|
wrap.className = 'col-md-12';
|
||||||
|
var al = document.createElement('div');
|
||||||
|
al.className = 'alert alert-danger';
|
||||||
|
var title = document.createElement('strong');
|
||||||
|
title.textContent = 'Unsupported Browser';
|
||||||
|
var msg = document.createElement('p');
|
||||||
|
msg.textContent = 'It looks like your browser does not support ' +
|
||||||
|
'the required JavaScript features to run ' +
|
||||||
|
'CyTube. This is usually caused by ' +
|
||||||
|
'using an outdated browser version. Please '+
|
||||||
|
'check if an update is available. Your ' +
|
||||||
|
'browser version is reported as:';
|
||||||
|
var version = document.createElement('tt');
|
||||||
|
version.textContent = navigator.userAgent;
|
||||||
|
|
||||||
|
wrap.appendChild(al);
|
||||||
|
al.appendChild(title);
|
||||||
|
al.appendChild(msg);
|
||||||
|
al.appendChild(document.createElement('br'));
|
||||||
|
al.appendChild(version);
|
||||||
|
document.getElementById('motdrow').appendChild(wrap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error probing for feature support:', e.stack);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
var CL_VERSION = 3.0;
|
var CL_VERSION = 3.0;
|
||||||
var GS_VERSION = 1.7; // Google Drive Userscript
|
var GS_VERSION = 1.7; // Google Drive Userscript
|
||||||
|
|
||||||
|
@ -48,7 +100,8 @@ var CHATMAXSIZE = 100;
|
||||||
var SCROLLCHAT = true;
|
var SCROLLCHAT = true;
|
||||||
var IGNORE_SCROLL_EVENT = false;
|
var IGNORE_SCROLL_EVENT = false;
|
||||||
var LASTCHAT = {
|
var LASTCHAT = {
|
||||||
name: ""
|
name: "",
|
||||||
|
time: 0
|
||||||
};
|
};
|
||||||
var FOCUSED = true;
|
var FOCUSED = true;
|
||||||
var PAGETITLE = "CyTube";
|
var PAGETITLE = "CyTube";
|
||||||
|
@ -112,33 +165,37 @@ function getOrDefault(k, def) {
|
||||||
var IGNORED = getOrDefault("ignorelist", []);
|
var IGNORED = getOrDefault("ignorelist", []);
|
||||||
|
|
||||||
var USEROPTS = {
|
var USEROPTS = {
|
||||||
|
// General tab
|
||||||
theme : getOrDefault("theme", DEFAULT_THEME), // Set in head template
|
theme : getOrDefault("theme", DEFAULT_THEME), // Set in head template
|
||||||
layout : getOrDefault("layout", "fluid"),
|
layout : getOrDefault("layout", "fluid"),
|
||||||
synch : getOrDefault("synch", true),
|
|
||||||
hidevid : getOrDefault("hidevid", false),
|
|
||||||
show_timestamps : getOrDefault("show_timestamps", true),
|
|
||||||
modhat : getOrDefault("modhat", false),
|
|
||||||
blink_title : getOrDefault("blink_title", "onlyping"),
|
|
||||||
sync_accuracy : getOrDefault("sync_accuracy", 2),
|
|
||||||
wmode_transparent : getOrDefault("wmode_transparent", true),
|
|
||||||
chatbtn : getOrDefault("chatbtn", false),
|
|
||||||
altsocket : getOrDefault("altsocket", false),
|
|
||||||
qbtn_hide : getOrDefault("qbtn_hide", false),
|
|
||||||
qbtn_idontlikechange : getOrDefault("qbtn_idontlikechange", false),
|
|
||||||
first_visit : getOrDefault("first_visit", true),
|
|
||||||
ignore_channelcss : getOrDefault("ignore_channelcss", false),
|
ignore_channelcss : getOrDefault("ignore_channelcss", false),
|
||||||
ignore_channeljs : getOrDefault("ignore_channeljs", false),
|
ignore_channeljs : getOrDefault("ignore_channeljs", false),
|
||||||
|
// Playback tab
|
||||||
|
synch : getOrDefault("synch", true),
|
||||||
|
sync_accuracy : getOrDefault("sync_accuracy", 2),
|
||||||
|
hidevid : getOrDefault("hidevid", false),
|
||||||
|
default_quality : getOrDefault("default_quality", "auto"),
|
||||||
|
qbtn_hide : getOrDefault("qbtn_hide", false),
|
||||||
|
qbtn_idontlikechange : getOrDefault("qbtn_idontlikechange", false),
|
||||||
|
peertube_risk : getOrDefault("peertube_risk", false),
|
||||||
|
// Chat tab
|
||||||
|
show_timestamps : getOrDefault("show_timestamps", true),
|
||||||
sort_rank : getOrDefault("sort_rank", true),
|
sort_rank : getOrDefault("sort_rank", true),
|
||||||
sort_afk : getOrDefault("sort_afk", false),
|
sort_afk : getOrDefault("sort_afk", false),
|
||||||
default_quality : getOrDefault("default_quality", "auto"),
|
blink_title : getOrDefault("blink_title", "onlyping"),
|
||||||
boop : getOrDefault("boop", "never"),
|
boop : getOrDefault("boop", "never"),
|
||||||
show_shadowchat : getOrDefault("show_shadowchat", false),
|
notifications : getOrDefault("notifications", "never"),
|
||||||
emotelist_sort : getOrDefault("emotelist_sort", true),
|
chatbtn : getOrDefault("chatbtn", false),
|
||||||
no_emotes : getOrDefault("no_emotes", false),
|
no_emotes : getOrDefault("no_emotes", false),
|
||||||
strip_image : getOrDefault("strip_image", false),
|
strip_image : getOrDefault("strip_image", false),
|
||||||
chat_tab_method : getOrDefault("chat_tab_method", "Cycle options"),
|
chat_tab_method : getOrDefault("chat_tab_method", "Cycle options"),
|
||||||
notifications : getOrDefault("notifications", "never"),
|
// Moderator tab
|
||||||
show_ip_in_tooltip : getOrDefault("show_ip_in_tooltip", true)
|
modhat : getOrDefault("modhat", false),
|
||||||
|
show_shadowchat : getOrDefault("show_shadowchat", false),
|
||||||
|
show_ip_in_tooltip : getOrDefault("show_ip_in_tooltip", true),
|
||||||
|
// Elsewhere
|
||||||
|
first_visit : getOrDefault("first_visit", true),
|
||||||
|
emotelist_sort : getOrDefault("emotelist_sort", true),
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Backwards compatibility check */
|
/* Backwards compatibility check */
|
||||||
|
@ -179,7 +236,6 @@ if (["never", "onlyping", "always"].indexOf(USEROPTS.boop) === -1) {
|
||||||
|
|
||||||
var VOLUME = parseFloat(getOrDefault("volume", 1));
|
var VOLUME = parseFloat(getOrDefault("volume", 1));
|
||||||
|
|
||||||
var NO_WEBSOCKETS = USEROPTS.altsocket;
|
|
||||||
var NO_VIMEO = Boolean(location.host.match("cytu.be"));
|
var NO_VIMEO = Boolean(location.host.match("cytu.be"));
|
||||||
|
|
||||||
var JSPREF = getOpt("channel_js_pref") || {};
|
var JSPREF = getOpt("channel_js_pref") || {};
|
||||||
|
|
4
www/js/jquery-1.11.0.min.js
vendored
4
www/js/jquery-1.11.0.min.js
vendored
File diff suppressed because one or more lines are too long
5
www/js/jquery-1.12.4.min.js
vendored
Normal file
5
www/js/jquery-1.12.4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
22523
www/js/jquery-ui.js
vendored
22523
www/js/jquery-ui.js
vendored
File diff suppressed because it is too large
Load diff
237
www/js/niconico.js
Normal file
237
www/js/niconico.js
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
/*
|
||||||
|
* Niconico iframe embed api
|
||||||
|
* Written by Xaekai
|
||||||
|
* Copyright (c) 2022 Radiant Feather; Licensed AGPLv3
|
||||||
|
*
|
||||||
|
* Dual-licensed MIT when distributed with CyTube/sync.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class NicovideoEmbed {
|
||||||
|
static origin = 'https://embed.nicovideo.jp';
|
||||||
|
static methods = [
|
||||||
|
'loadComplete',
|
||||||
|
'mute',
|
||||||
|
'pause',
|
||||||
|
'play',
|
||||||
|
'seek',
|
||||||
|
'volumeChange',
|
||||||
|
];
|
||||||
|
static frames = [
|
||||||
|
'error',
|
||||||
|
'loadComplete',
|
||||||
|
'playerMetadataChange',
|
||||||
|
'playerStatusChange',
|
||||||
|
'seekStatusChange',
|
||||||
|
'statusChange',
|
||||||
|
//'player-error:video:play',
|
||||||
|
//'player-error:video:seek',
|
||||||
|
];
|
||||||
|
static events = [
|
||||||
|
'ended',
|
||||||
|
'error',
|
||||||
|
'muted',
|
||||||
|
'pause',
|
||||||
|
'play',
|
||||||
|
'progress',
|
||||||
|
'ready',
|
||||||
|
'timeupdate',
|
||||||
|
'unmuted',
|
||||||
|
'volumechange',
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
this.handlers = Object.fromEntries(NicovideoEmbed.frames.map(key => [key,[]]));
|
||||||
|
this.listeners = Object.fromEntries(NicovideoEmbed.events.map(key => [key,[]]));
|
||||||
|
this.state = ({
|
||||||
|
ready: false,
|
||||||
|
playerStatus: 1,
|
||||||
|
currentTime: 0.0, // ms
|
||||||
|
muted: false,
|
||||||
|
volume: 0.99,
|
||||||
|
maximumBuffered: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupHandlers();
|
||||||
|
this.scaffold(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
scaffold({ iframe = null, playerId = 1, videoId = null }){
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.messageListener();
|
||||||
|
if(iframe === null){
|
||||||
|
if(videoId === null){
|
||||||
|
throw new Error('You must provide either an existing iframe or a videoId');
|
||||||
|
}
|
||||||
|
const iframe = this.iframe = document.createElement('iframe');
|
||||||
|
|
||||||
|
const source = new URL(`${NicovideoEmbed.origin}/watch/${videoId}`);
|
||||||
|
source.search = new URLSearchParams({
|
||||||
|
jsapi: 1,
|
||||||
|
autoplay: 1,
|
||||||
|
playerId
|
||||||
|
});
|
||||||
|
iframe.setAttribute('src', source);
|
||||||
|
iframe.setAttribute('id', playerId);
|
||||||
|
iframe.setAttribute('allow', 'autoplay; fullscreen');
|
||||||
|
iframe.addEventListener('load', ()=>{
|
||||||
|
this.observe();
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.iframe = iframe;
|
||||||
|
this.observe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupHandlers() {
|
||||||
|
this.handlers.loadComplete.push((data) => {
|
||||||
|
this.emit('ready');
|
||||||
|
this.state.ready = true;
|
||||||
|
Object.assign(this, data);
|
||||||
|
});
|
||||||
|
this.handlers.error.push((data) => {
|
||||||
|
this.emit('error', data);
|
||||||
|
});
|
||||||
|
this.handlers.playerStatusChange.push((data) => {
|
||||||
|
let event;
|
||||||
|
switch (data.playerStatus) {
|
||||||
|
case 1: /* Buffering */ return;
|
||||||
|
case 2: event = 'play'; break;
|
||||||
|
case 3: event = 'pause'; break;
|
||||||
|
case 4: event = 'ended'; break;
|
||||||
|
}
|
||||||
|
this.state.playerStatus = data.playerStatus;
|
||||||
|
this.emit(event);
|
||||||
|
});
|
||||||
|
this.handlers.playerMetadataChange.push(({ currentTime, volume, muted, maximumBuffered }) => {
|
||||||
|
const self = this.state;
|
||||||
|
|
||||||
|
if (currentTime !== self.currentTime) {
|
||||||
|
self.currentTime = currentTime;
|
||||||
|
this.emit('timeupdate', currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (muted !== self.muted) {
|
||||||
|
self.muted = muted;
|
||||||
|
this.emit(muted ? 'muted' : 'unmuted');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (volume !== self.volume) {
|
||||||
|
self.volume = volume;
|
||||||
|
this.emit('volumechange', volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maximumBuffered !== self.maximumBuffered) {
|
||||||
|
self.maximumBuffered = maximumBuffered;
|
||||||
|
this.emit('progress', maximumBuffered);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.handlers.seekStatusChange.push((data) => {
|
||||||
|
//
|
||||||
|
});
|
||||||
|
this.handlers.statusChange.push((data) => {
|
||||||
|
//
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
messageListener() {
|
||||||
|
const dispatcher = (event) => {
|
||||||
|
if (event.origin === NicovideoEmbed.origin && event.data.playerId === this.playerId) {
|
||||||
|
const { data } = event.data;
|
||||||
|
this.dispatch(event.data.eventName, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('message', dispatcher);
|
||||||
|
|
||||||
|
/* Clean up */
|
||||||
|
this.observer = new MutationObserver((alterations) => {
|
||||||
|
alterations.forEach((change) => {
|
||||||
|
change.removedNodes.forEach((deletion) => {
|
||||||
|
if(deletion.nodeName === 'IFRAME') {
|
||||||
|
window.removeEventListener('message', dispatcher)
|
||||||
|
this.observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
observe(){
|
||||||
|
this.state.receptive = true;
|
||||||
|
this.observer.observe(this.iframe.parentElement, { subtree: true, childList: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(frame, data = null){
|
||||||
|
if(!NicovideoEmbed.frames.includes(frame)){
|
||||||
|
console.error(JSON.stringify(data, undefined, 4));
|
||||||
|
throw new Error(`NicovideoEmbed ${frame}`);
|
||||||
|
}
|
||||||
|
[...this.handlers[frame]].forEach(handler => {
|
||||||
|
handler.call(this, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event, data = null){
|
||||||
|
[...this.listeners[event]].forEach(listener => {
|
||||||
|
listener.call(this, data);
|
||||||
|
});
|
||||||
|
if(event === 'ready'){
|
||||||
|
this.listeners.ready.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage(request) {
|
||||||
|
if(!this.state.receptive){
|
||||||
|
setTimeout(() => { this.postMessage(request) }, 1000 / 24);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = Object.assign({
|
||||||
|
sourceConnectorType: 1,
|
||||||
|
playerId: this.playerId
|
||||||
|
}, request);
|
||||||
|
|
||||||
|
this.iframe.contentWindow.postMessage(message, NicovideoEmbed.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, listener){
|
||||||
|
if(!NicovideoEmbed.events.includes(event)){
|
||||||
|
throw new Error('Unrecognized event name');
|
||||||
|
}
|
||||||
|
if(event === 'ready'){
|
||||||
|
if(this.state.ready){
|
||||||
|
listener();
|
||||||
|
return this;
|
||||||
|
} else {
|
||||||
|
setTimeout(() => { this.loadComplete() }, 1000 / 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.listeners[event].push(listener);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
mute(state){
|
||||||
|
this.postMessage({ eventName: 'mute', data: { mute: state } });
|
||||||
|
}
|
||||||
|
|
||||||
|
pause(){
|
||||||
|
this.postMessage({ eventName: 'pause' });
|
||||||
|
}
|
||||||
|
|
||||||
|
play(){
|
||||||
|
this.postMessage({ eventName: 'play' });
|
||||||
|
}
|
||||||
|
|
||||||
|
loadComplete(){
|
||||||
|
this.postMessage({ eventName: 'loadComplete' });
|
||||||
|
}
|
||||||
|
|
||||||
|
seek(ms){
|
||||||
|
this.postMessage({ eventName: 'seek', data: { time: ms } });
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeChange(volume){
|
||||||
|
this.postMessage({ eventName: 'volumeChange', data: { volume } });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
window.NicovideoEmbed = NicovideoEmbed;
|
BIN
www/js/octopus/subtitles-octopus-worker-legacy.data
Normal file
BIN
www/js/octopus/subtitles-octopus-worker-legacy.data
Normal file
Binary file not shown.
35
www/js/octopus/subtitles-octopus-worker-legacy.js
Normal file
35
www/js/octopus/subtitles-octopus-worker-legacy.js
Normal file
File diff suppressed because one or more lines are too long
BIN
www/js/octopus/subtitles-octopus-worker-legacy.js.mem
Normal file
BIN
www/js/octopus/subtitles-octopus-worker-legacy.js.mem
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue