diff --git a/bin/admin.js b/bin/admin.js old mode 100644 new mode 100755 index 8f9478a1..1839a49f --- a/bin/admin.js +++ b/bin/admin.js @@ -49,24 +49,107 @@ let commands = [ 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); + } - if (/^[yY]$/.test(answer)) { - let res = await doCommand({ - command: 'ban-channel', - name, - externalReason, - internalReason - }); + let res = await doCommand({ + command: 'ban-channel', + name, + externalReason, + internalReason + }); - console.log(`Status: ${res.status}`); - if (res.status === 'error') { + switch (res.status) { + case 'error': console.log('Error:', res.error); process.exit(1); - } else { + break; + case 'success': + console.log('Ban succeeded.'); process.exit(0); - } - } else { + 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 '); + 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 '); + 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; } } } diff --git a/src/cli/banned-channels.js b/src/cli/banned-channels.js index 06028e95..b88cfdca 100644 --- a/src/cli/banned-channels.js +++ b/src/cli/banned-channels.js @@ -10,3 +10,15 @@ export async function handleBanChannel({ name, externalReason, internalReason }) 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 }; +} diff --git a/src/config.js b/src/config.js index 0fed443c..adb1133f 100644 --- a/src/config.js +++ b/src/config.js @@ -66,7 +66,6 @@ var defaults = { } }, "youtube-v3-key": "", - "channel-blacklist": [], "channel-path": "r", "channel-save-interval": 5, "max-channels-per-user": 5, @@ -372,13 +371,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 */ if(!/^[-\w]+$/.test(cfg["channel-path"])){ LOGGER.error("Channel paths may only use the same characters as usernames and channel names."); diff --git a/src/controller/banned-channels.js b/src/controller/banned-channels.js index ff524b70..197c07fc 100644 --- a/src/controller/banned-channels.js +++ b/src/controller/banned-channels.js @@ -1,3 +1,4 @@ +import { eventlog } from '../logger'; const LOGGER = require('@calzoneman/jsli')('BannedChannelsController'); export class BannedChannelsController { @@ -6,8 +7,13 @@ export class BannedChannelsController { this.globalMessageBus = globalMessageBus; } + /* + * 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) { @@ -26,4 +32,61 @@ export class BannedChannelsController { { channel: name, externalReason } ); } + + async unbanChannel(name, unbannedBy) { + LOGGER.info(`Unbanning channel ${name}`); + eventlog.log(`[acp] ${unbannedBy} unbanned channel ${name}`); + + this.globalMessageBus.emit( + 'ChannelUnbanned', + { channel: name } + ); + + await this.dbChannels.removeBannedChannel(name); + } + + async getBannedChannel(name) { + // TODO: cache + return this.dbChannels.getBannedChannel(name); + } +} + +class Cache { + constructor({ maxElem, maxAge }) { + this.maxElem = maxElem; + this.maxAge = maxAge; + this.cache = new Map(); + } + + 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()); + } + } + + get(key) { + let val = this.cache.get(key); + + if (val != null) { + 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); + } + } + } } diff --git a/src/database/channels.js b/src/database/channels.js index a78ceb40..3f5960c4 100644 --- a/src/database/channels.js +++ b/src/database/channels.js @@ -763,10 +763,22 @@ module.exports = { banned_by: bannedBy }); let update = tx.raw(createMySQLDuplicateKeyUpdate( - ['external_reason', 'internal_reason'] + ['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(); + }); } }; diff --git a/src/main.js b/src/main.js index 6b240fbf..04e06a5c 100644 --- a/src/main.js +++ b/src/main.js @@ -33,7 +33,11 @@ async function handleCliCmd(cmd) { try { switch (cmd.command) { case 'ban-channel': - return await bannedChannels.handleBanChannel(cmd); + 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}"`); } @@ -52,6 +56,7 @@ function handleLine(line, client) { 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) { } diff --git a/src/user.js b/src/user.js index 88ddab6e..39cae90b 100644 --- a/src/user.js +++ b/src/user.js @@ -76,10 +76,6 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) { } 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, () => { var chan; diff --git a/src/web/webserver.js b/src/web/webserver.js index d3ae2ec1..aeb460b5 100644 --- a/src/web/webserver.js +++ b/src/web/webserver.js @@ -198,8 +198,8 @@ module.exports = { app, ioConfig, chanPath, - async name => null - /*require('../database/channels').getBannedChannel*/ + // TODO: banController + require('../database/channels').getBannedChannel ); require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries()); require('./routes/socketconfig')(app, clusterClient);