Compare commits
40 commits
postgreSQL
...
renaissanc
Author | SHA1 | Date | |
---|---|---|---|
b9f66a8580 | |||
9d9f638496 | |||
0fe2d9afb9 | |||
f7972af085 | |||
ff47583e06 | |||
0e410e4f2d | |||
e713eca9cc | |||
5ecca27c9f | |||
568b7f3cfe | |||
ef0a04ddcb | |||
0226d73272 | |||
24fdc3639a | |||
9bd8fee92f | |||
e2944e0769 | |||
94e2fd10ad | |||
dd051098bc | |||
094c9a7c4e | |||
85118db86e | |||
6b39d754d3 | |||
89d79fe422 | |||
8da1d1c772 | |||
c506ccf05b | |||
8dde08ac6d | |||
6a0119fa17 | |||
b7188832da | |||
a92df0c6d9 | |||
9a639654f4 | |||
6812884760 | |||
d3aed7121b | |||
eb71718bea | |||
5e1cfc41d9 | |||
67b61d69dc | |||
cfe009323f | |||
3212aa5df6 | |||
af6e7bed0c | |||
46fcec8d14 | |||
614a039266 | |||
f365d4192a | |||
e1c7b76650 | |||
34e2130ce6 |
|
@ -1,6 +0,0 @@
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
indent_size = 4
|
|
||||||
indent_style = space
|
|
21
NEWS.md
21
NEWS.md
|
@ -1,24 +1,3 @@
|
||||||
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
|
2022-08-28
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
176
bin/admin.js
176
bin/admin.js
|
@ -1,176 +0,0 @@
|
||||||
#!/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);
|
|
||||||
}
|
|
|
@ -128,8 +128,6 @@ 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 -
|
||||||
|
|
|
@ -110,25 +110,6 @@ 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') {
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
11024
package-lock.json
generated
11024
package-lock.json
generated
File diff suppressed because it is too large
Load diff
17
package.json
17
package.json
|
@ -2,17 +2,17 @@
|
||||||
"author": "Calvin Montgomery",
|
"author": "Calvin Montgomery",
|
||||||
"name": "CyTube",
|
"name": "CyTube",
|
||||||
"description": "Online media synchronizer and chat",
|
"description": "Online media synchronizer and chat",
|
||||||
"version": "3.86.0",
|
"version": "3.83.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": "github:CyTube/mediaquery#564d0c4615e80f72722b0f68ac81f837a4c5fc81",
|
"@cytube/mediaquery": "github:CyTube/mediaquery#c1dcf792cd6e9977c04c1e96f23315dad5e3294d",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.1",
|
"body-parser": "^1.19.0",
|
||||||
"cheerio": "^1.0.0-rc.10",
|
"cheerio": "^1.0.0-rc.10",
|
||||||
"clone": "^2.1.2",
|
"clone": "^2.1.2",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
|
@ -20,22 +20,21 @@
|
||||||
"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.18.2",
|
"express": "^4.17.1",
|
||||||
"express-minify": "^1.0.0",
|
"express-minify": "^1.0.0",
|
||||||
"json-typecheck": "^0.1.3",
|
"json-typecheck": "^0.1.3",
|
||||||
"knex": "^2.4.0",
|
"knex": "^0.95.2",
|
||||||
"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.6.1",
|
||||||
"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.1.1",
|
"redis": "^3.1.1",
|
||||||
"sanitize-html": "^2.7.0",
|
"sanitize-html": "^2.7.0",
|
||||||
"serve-static": "^1.15.0",
|
"serve-static": "^1.14.1",
|
||||||
"socket.io": "^4.5.4",
|
"socket.io": "^4.5.0",
|
||||||
"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",
|
||||||
|
|
|
@ -15,19 +15,8 @@ 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>"
|
||||||
link = document.createElement('a')
|
alert = makeAlert('Untrusted Content', CUSTOM_EMBED_WARNING.replace('%link%', link),
|
||||||
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')
|
||||||
|
|
|
@ -94,10 +94,7 @@ 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.code === 'EBANNED') {
|
if (err && err !== "Channel is not registered") {
|
||||||
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 {
|
||||||
|
|
|
@ -162,7 +162,7 @@ ChatModule.prototype.handleChatMsg = function (user, data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.msg = data.msg.substring(0, Config.get("max-chat-message-length"));
|
data.msg = data.msg.substring(0, 320);
|
||||||
|
|
||||||
// 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)) {
|
||||||
|
@ -248,7 +248,7 @@ ChatModule.prototype.handlePm = function (user, data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
data.msg = data.msg.substring(0, Config.get("max-chat-message-length"));
|
data.msg = data.msg.substring(0, 320);
|
||||||
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) {
|
||||||
|
|
|
@ -4,7 +4,6 @@ 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);
|
||||||
|
@ -262,6 +261,7 @@ 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,10 +276,6 @@ 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;
|
||||||
|
@ -327,9 +323,6 @@ 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);
|
||||||
|
|
||||||
|
@ -411,12 +404,7 @@ 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).catch(error => {
|
promises.push(this.banName(actor, name, reason));
|
||||||
// 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);
|
||||||
|
@ -452,9 +440,8 @@ 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 " + XSS.sanitizeText(data.name),
|
user.getName() + " unbanned " + data.name,
|
||||||
banperm
|
banperm
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
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,8 +11,7 @@ import { CaptchaConfig } from './configuration/captchaconfig';
|
||||||
const LOGGER = require('@calzoneman/jsli')('config');
|
const LOGGER = require('@calzoneman/jsli')('config');
|
||||||
|
|
||||||
var defaults = {
|
var defaults = {
|
||||||
database: {
|
mysql: {
|
||||||
client: "mysql",
|
|
||||||
server: "localhost",
|
server: "localhost",
|
||||||
port: 3306,
|
port: 3306,
|
||||||
database: "cytube3",
|
database: "cytube3",
|
||||||
|
@ -67,12 +66,12 @@ var defaults = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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
|
||||||
|
@ -373,6 +372,13 @@ 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.");
|
||||||
|
@ -429,11 +435,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,6 +31,10 @@ const SOURCE_CONTENT_TYPES = new Set([
|
||||||
'video/webm'
|
'video/webm'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const LIVE_ONLY_CONTENT_TYPES = new Set([
|
||||||
|
'application/dash+xml'
|
||||||
|
]);
|
||||||
|
|
||||||
const AUDIO_ONLY_CONTENT_TYPES = new Set([
|
const AUDIO_ONLY_CONTENT_TYPES = new Set([
|
||||||
'audio/aac',
|
'audio/aac',
|
||||||
'audio/mp4',
|
'audio/mp4',
|
||||||
|
@ -169,7 +173,7 @@ export function validate(data) {
|
||||||
validateURL(data.thumbnail);
|
validateURL(data.thumbnail);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateSources(data.sources);
|
validateSources(data.sources, data);
|
||||||
validateAudioTracks(data.audioTracks);
|
validateAudioTracks(data.audioTracks);
|
||||||
validateTextTracks(data.textTracks);
|
validateTextTracks(data.textTracks);
|
||||||
/*
|
/*
|
||||||
|
@ -182,7 +186,7 @@ export function validate(data) {
|
||||||
validateFonts(data.fonts);
|
validateFonts(data.fonts);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateSources(sources) {
|
function validateSources(sources, data) {
|
||||||
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)
|
||||||
|
@ -198,6 +202,10 @@ function validateSources(sources) {
|
||||||
`unacceptable source contentType "${source.contentType}"`
|
`unacceptable source contentType "${source.contentType}"`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (LIVE_ONLY_CONTENT_TYPES.has(source.contentType) && !data.live)
|
||||||
|
throw new ValidationError(
|
||||||
|
`contentType "${source.contentType}" requires live: true`
|
||||||
|
);
|
||||||
// TODO (Xaekai): This should be allowed
|
// TODO (Xaekai): This should be allowed
|
||||||
if (/*!AUDIO_ONLY_CONTENT_TYPES.has(source.contentType) && */!SOURCE_QUALITIES.has(source.quality))
|
if (/*!AUDIO_ONLY_CONTENT_TYPES.has(source.contentType) && */!SOURCE_QUALITIES.has(source.quality))
|
||||||
throw new ValidationError(`unacceptable source quality "${source.quality}"`);
|
throw new ValidationError(`unacceptable source quality "${source.quality}"`);
|
||||||
|
|
|
@ -30,19 +30,19 @@ class Database {
|
||||||
constructor(knexConfig = null) {
|
constructor(knexConfig = null) {
|
||||||
if (knexConfig === null) {
|
if (knexConfig === null) {
|
||||||
knexConfig = {
|
knexConfig = {
|
||||||
client: Config.get('database.client'),
|
client: 'mysql',
|
||||||
connection: {
|
connection: {
|
||||||
host: Config.get('database.server'),
|
host: Config.get('mysql.server'),
|
||||||
port: Config.get('database.port'),
|
port: Config.get('mysql.port'),
|
||||||
user: Config.get('database.user'),
|
user: Config.get('mysql.user'),
|
||||||
password: Config.get('database.password'),
|
password: Config.get('mysql.password'),
|
||||||
database: Config.get('database.database'),
|
database: Config.get('mysql.database'),
|
||||||
multipleStatements: true, // Legacy thing
|
multipleStatements: true, // Legacy thing
|
||||||
charset: 'utf8mb4'
|
charset: 'utf8mb4'
|
||||||
},
|
},
|
||||||
pool: {
|
pool: {
|
||||||
min: Config.get('database.pool-size'),
|
min: Config.get('mysql.pool-size'),
|
||||||
max: Config.get('database.pool-size')
|
max: Config.get('mysql.pool-size')
|
||||||
},
|
},
|
||||||
debug: !!process.env.KNEX_DEBUG
|
debug: !!process.env.KNEX_DEBUG
|
||||||
};
|
};
|
||||||
|
@ -73,8 +73,6 @@ 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);
|
||||||
|
|
|
@ -2,7 +2,6 @@ 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';
|
||||||
|
|
||||||
|
@ -210,9 +209,7 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query("SELECT c.*, bc.external_reason as banReason " +
|
db.query("SELECT * FROM `channels` WHERE owner=?", [owner],
|
||||||
"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, []);
|
||||||
|
@ -248,28 +245,13 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query("SELECT c.*, bc.external_reason as banReason " +
|
db.query("SELECT * FROM `channels` WHERE name=?", chan.name, function (err, res) {
|
||||||
"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 && res[0].banReason !== null) {
|
if (res.length === 0) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -722,63 +704,5 @@ 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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -156,15 +156,4 @@ 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
36
src/main.js
36
src/main.js
|
@ -2,7 +2,6 @@ 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');
|
||||||
|
|
||||||
|
@ -29,39 +28,10 @@ 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, client) {
|
function handleLine(line) {
|
||||||
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 {
|
||||||
|
@ -111,9 +81,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, client) => {
|
line => {
|
||||||
try {
|
try {
|
||||||
handleLine(line, client);
|
handleLine(line);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Error in UNIX socket command handler: %s',
|
'Error in UNIX socket command handler: %s',
|
||||||
|
|
|
@ -48,7 +48,6 @@ 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;
|
||||||
|
@ -72,7 +71,6 @@ 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 ------------------------------------------------------
|
||||||
|
@ -110,11 +108,6 @@ 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);
|
||||||
|
@ -141,8 +134,7 @@ 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 -----------------------------------------
|
||||||
|
@ -557,34 +549,6 @@ 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(), stream);
|
this.handler(msg.toString());
|
||||||
});
|
});
|
||||||
}).listen(this.socket);
|
}).listen(this.socket);
|
||||||
process.on('exit', this.closeServiceSocket.bind(this));
|
process.on('exit', this.closeServiceSocket.bind(this));
|
||||||
|
|
|
@ -76,6 +76,10 @@ 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;
|
||||||
|
@ -98,6 +102,10 @@ 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}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
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 };
|
|
|
@ -265,8 +265,6 @@ 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", {
|
||||||
|
@ -276,14 +274,6 @@ 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,
|
||||||
|
|
|
@ -1,25 +1,16 @@
|
||||||
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, getBannedChannel) {
|
export default function initialize(app, ioConfig, chanPath) {
|
||||||
app.get(`/${chanPath}/:channel`, async (req, res) => {
|
app.get(`/${chanPath}/:channel`, (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');
|
||||||
|
@ -28,8 +19,7 @@ export default function initialize(app, ioConfig, chanPath, getBannedChannel) {
|
||||||
|
|
||||||
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")
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,8 +143,7 @@ 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');
|
||||||
|
@ -195,12 +194,7 @@ module.exports = {
|
||||||
LOGGER.info('Enabled express-minify for CSS and JS');
|
LOGGER.info('Enabled express-minify for CSS and JS');
|
||||||
}
|
}
|
||||||
|
|
||||||
require('./routes/channel')(
|
require('./routes/channel')(app, ioConfig, chanPath);
|
||||||
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);
|
||||||
|
|
|
@ -24,7 +24,7 @@ block content
|
||||||
tbody
|
tbody
|
||||||
for c in channels
|
for c in channels
|
||||||
tr
|
tr
|
||||||
td
|
th
|
||||||
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,9 +32,6 @@ 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
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
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=maxMsgLen, style="display: none")
|
input#chatline.form-control(type="text", maxlength="320", 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")
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,55 +1,4 @@
|
||||||
/*eslint no-unused-vars: "off"*/
|
/*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
|
||||||
|
|
||||||
|
|
|
@ -1287,11 +1287,6 @@ function playlistMove(from, after, cb) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkYP(id) {
|
|
||||||
if (!/^(PL[a-zA-Z0-9_-]{32}|PL[A-F0-9]{16}|OLA[a-zA-Z0-9_-]{38})$/.test(id)) {
|
|
||||||
throw new Error('Invalid YouTube Playlist ID. Note that only regular user-created playlists are supported.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMediaLink(url) {
|
function parseMediaLink(url) {
|
||||||
function parseShortCode(url){
|
function parseShortCode(url){
|
||||||
|
@ -1306,9 +1301,6 @@ function parseMediaLink(url) {
|
||||||
case 'fi':
|
case 'fi':
|
||||||
case 'cm':
|
case 'cm':
|
||||||
return { type, id };
|
return { type, id };
|
||||||
case 'yp':
|
|
||||||
checkYP(id);
|
|
||||||
return { type, id };
|
|
||||||
// Generic for the rest.
|
// Generic for the rest.
|
||||||
default:
|
default:
|
||||||
return { type, id: id.match(/([^\?&#]+)/)[1] };
|
return { type, id: id.match(/([^\?&#]+)/)[1] };
|
||||||
|
@ -1364,7 +1356,6 @@ function parseMediaLink(url) {
|
||||||
return { type: 'yt', id: data.pathname.slice(8,19) }
|
return { type: 'yt', id: data.pathname.slice(8,19) }
|
||||||
}
|
}
|
||||||
if(data.pathname == '/playlist'){
|
if(data.pathname == '/playlist'){
|
||||||
checkYP(data.searchParams.get('list'));
|
|
||||||
return { type: 'yp', id: data.searchParams.get('list') }
|
return { type: 'yp', id: data.searchParams.get('list') }
|
||||||
}
|
}
|
||||||
case 'youtu.be':
|
case 'youtu.be':
|
||||||
|
|
Loading…
Reference in a new issue