diff --git a/chat-plugins/COMMANDS.md b/chat-plugins/COMMANDS.md index 473568ca24..4e24be8a98 100644 --- a/chat-plugins/COMMANDS.md +++ b/chat-plugins/COMMANDS.md @@ -17,7 +17,7 @@ This is called an alias: it makes it so `/ip` does the same thing as But to actually define a command, it's a function: avatars: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox('You can by clicking on it in the QuestionsBug Reports"); } else { @@ -953,7 +953,7 @@ exports.commands = { }, avatars: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox("You can by clicking on it in the menu in the upper right. Custom avatars are only obtainable by staff."); }, avatarshelp: ["/avatars - Explains how to change avatars.", @@ -961,7 +961,7 @@ exports.commands = { introduction: 'intro', intro: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox( "New to competitive Pokémon?
" + "- Beginner's Guide to Pokémon Showdown
" + @@ -976,7 +976,7 @@ exports.commands = { mentoring: 'smogintro', smogonintro: 'smogintro', smogintro: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox( "Welcome to Smogon's official simulator! The Smogon Info / Intro Hub can help you get integrated into the community.
" + "- Useful Smogon Info
" + @@ -986,7 +986,7 @@ exports.commands = { calculator: 'calc', calc: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox( "Pokémon Showdown! damage calculator. (Courtesy of Honko)
" + "- Damage Calculator" @@ -997,7 +997,7 @@ exports.commands = { capintro: 'cap', cap: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox( "An introduction to the Create-A-Pokémon project:
" + "- CAP project website and description
" + @@ -1010,7 +1010,7 @@ exports.commands = { "!cap - Show everyone that information. Requires: + % @ # & ~"], gennext: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox( "NEXT (also called Gen-NEXT) is a mod that makes changes to the game:
" + "- README: overview of NEXT
" + @@ -1022,7 +1022,7 @@ exports.commands = { om: 'othermetas', othermetas: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; target = toId(target); let buffer = ""; @@ -1065,7 +1065,7 @@ exports.commands = { tiershelp: 'formathelp', formatshelp: 'formathelp', formathelp: function (target, room, user, connection, cmd) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (!target) { return this.sendReplyBox( "- Smogon Tiers
" + @@ -1128,7 +1128,7 @@ exports.commands = { roomhelp: function (target, room, user) { if (room.id === 'lobby' || room.battle) return this.sendReply("This command is too spammy for lobby/battles."); - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox( "Room drivers (%) can use:
" + "- /warn OR /k username: warn a user and show the Pokémon Showdown rules
" + @@ -1173,7 +1173,7 @@ exports.commands = { restarthelp: function (target, room, user) { if (room.id === 'lobby' && !this.can('lockdown')) return false; - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox( "The server is restarting. Things to know:
" + "- We wait a few minutes before restarting so people can finish up their battles
" + @@ -1205,7 +1205,7 @@ exports.commands = { rule: 'rules', rules: function (target, room, user) { if (!target) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox("Please follow the rules:
" + (room.rulesLink ? "- " + Tools.escapeHTML(room.title) + " room rules
" : "") + "- " + (room.rulesLink ? "Global rules" : "Rules") + ""); @@ -1229,7 +1229,7 @@ exports.commands = { "/rules [url] - Change the room rules URL. Requires: # & ~"], faq: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; target = target.toLowerCase(); let buffer = ""; let matched = false; @@ -1301,7 +1301,7 @@ exports.commands = { analysis: 'smogdex', strategy: 'smogdex', smogdex: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; let targets = target.split(','); let pokemon = Tools.getTemplate(targets[0]); @@ -1414,7 +1414,7 @@ exports.commands = { "!analysis [pokemon], [generation] - Shows everyone this link. Requires: + % @ # & ~"], veekun: function (target, broadcast, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; let baseLink = 'http://veekun.com/dex/'; @@ -1484,7 +1484,7 @@ exports.commands = { "!veekun [pokemon] - Shows everyone this link. Requires: + % @ # & ~"], register: function () { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox('You will be prompted to register upon winning a rated battle. Alternatively, there is a register button in the menu in the upper right.'); }, @@ -1509,7 +1509,7 @@ exports.commands = { roll: 'dice', dice: function (target, room, user) { if (!target || target.match(/[^d\d\s\-\+HL]/i)) return this.parse('/help dice'); - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; // ~30 is widely regarded as the sample size required for sum to be a Gaussian distribution. // This also sets a computation time constraint for safety. @@ -1602,7 +1602,7 @@ exports.commands = { pickrandom: function (target, room, user) { let options = target.split(','); if (options.length < 2) return this.parse('/help pick'); - if (!this.canBroadcast()) return false; + if (!this.runBroadcast()) return false; const pickedOption = options[Math.floor(Math.random() * options.length)]; return this.sendReplyBox('We randomly picked: ' + Tools.escapeHTML(pickedOption).trim()); }, @@ -1611,7 +1611,7 @@ exports.commands = { showimage: function (target, room, user) { if (!target) return this.parse('/help showimage'); if (!this.can('declare', null, room)) return false; - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (this.room.isPersonal && !this.user.can('announce')) { return this.errorReply("Images are not allowed in personal rooms."); } @@ -1663,7 +1663,7 @@ exports.commands = { if (message.charAt(0) === '!') this.broadcasting = true; } else { if (!this.can('declare', null, room)) return; - if (!this.canBroadcast(false, '!htmlbox')) return; + if (!this.runBroadcast('!htmlbox')) return; } this.sendReplyBox(target); diff --git a/chat-plugins/jeopardy.js b/chat-plugins/jeopardy.js index a32286cf3b..52b9b9e9fd 100644 --- a/chat-plugins/jeopardy.js +++ b/chat-plugins/jeopardy.js @@ -487,7 +487,7 @@ function renderGrid(questions, mode) { let commands = { help: function () { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox( "All commands are run under /jeopardy or /jp. For example, /jeopardy viewgrid.
" + @@ -510,7 +510,7 @@ let commands = { '': 'viewgrid', viewgrid: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; let jeopardy = jeopardies[room.id]; let questions = null; diff --git a/chat-plugins/poll.js b/chat-plugins/poll.js index 431f7288b4..3fd8e5490c 100644 --- a/chat-plugins/poll.js +++ b/chat-plugins/poll.js @@ -238,7 +238,7 @@ exports.commands = { room.add("The poll timer was turned on: the poll will end in " + timeout + " minute(s)."); return this.privateModCommand("(The poll timer was set to " + timeout + " minute(s) by " + user.name + ".)"); } else { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (room.poll.timeout) { return this.sendReply("The poll timer is on and will end in " + room.poll.timeoutMins + " minute(s)."); } else { @@ -272,7 +272,7 @@ exports.commands = { show: 'display', display: function (target, room, user, connection) { if (!room.poll) return this.errorReply("There is no poll running in this room."); - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; room.update(); if (this.broadcasting) { diff --git a/chat-plugins/scavengers.js b/chat-plugins/scavengers.js index 585e493a84..1ddef48f91 100644 --- a/chat-plugins/scavengers.js +++ b/chat-plugins/scavengers.js @@ -133,7 +133,7 @@ exports.commands = { scavengershelp: 'scavengerhelp', scavengerhelp: function (target, room, user) { if (room.id !== 'scavengers') return this.errorReply('This command can only be used in the Scavengers room.'); - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox( 'Player commands:
' + '- /scavengers - Join the scavengers room
' + diff --git a/chat-plugins/tcgtabletop.js b/chat-plugins/tcgtabletop.js index c4431f394f..ab052cd8c8 100644 --- a/chat-plugins/tcgtabletop.js +++ b/chat-plugins/tcgtabletop.js @@ -39,12 +39,12 @@ function wikiaSearch(subdomain, query, callback) { exports.commands = { ygo: 'yugioh', yugioh: function (target, room, user) { - if (!this.canBroadcast(true)) return; + if (!this.canBroadcast()) return; let subdomain = 'yugioh'; let query = target.trim(); wikiaSearch(subdomain, query, (err, data) => { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (err) { if (err instanceof SyntaxError || err.message === 'Malformed data') { if (!this.broadcasting) return this.sendReply("Error: Something went wrong in the request: " + err.message); diff --git a/chat-plugins/thehappyplace.js b/chat-plugins/thehappyplace.js index f36d526282..82946bd88d 100644 --- a/chat-plugins/thehappyplace.js +++ b/chat-plugins/thehappyplace.js @@ -14,7 +14,7 @@ exports.commands = { if (room.id !== 'thehappyplace') return this.errorReply("This command can only be used in The Happy Place."); if (!room.chatRoomData) return; if (!target) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (!room.chatRoomData.quote) return this.sendReplyBox("The Quote of the Day has not been set."); return this.sendReplyBox( "The current Inspirational Quote of the Day is:
" + diff --git a/chat-plugins/thestudio.js b/chat-plugins/thestudio.js index 2407f49e9e..4667a2c365 100644 --- a/chat-plugins/thestudio.js +++ b/chat-plugins/thestudio.js @@ -167,7 +167,7 @@ let commands = { return this.sendReplyBox(buffer); } - if (!this.canBroadcast()) return false; + if (!this.runBroadcast()) return false; if (!artistOfTheDay.nominations.size) return this.sendReplyBox("No nominations have been submitted yet."); let nominations = Array.from(artistOfTheDay.nominations).sort((a, b) => a[1].localeCompare(b[1])); @@ -225,7 +225,7 @@ let commands = { if (room.id !== 'thestudio') return this.errorReply('This command can only be used in The Studio.'); if (!room.chatRoomData) return false; if (!target) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (!room.chatRoomData.artistQuoteOfTheDay) return this.sendReplyBox("The Artist Quote of the Day has not been set."); return this.sendReplyBox( "The current Artist Quote of the Day is:
" + @@ -256,13 +256,13 @@ let commands = { '': function (target, room) { if (room.id !== 'thestudio') return this.errorReply('This command can only be used in The Studio.'); - if (!room.chatRoomData || !this.canBroadcast()) return false; + if (!room.chatRoomData || !this.runBroadcast()) return false; this.sendReplyBox("The Artist of the Day " + (room.chatRoomData.artistOfTheDay ? "is " + room.chatRoomData.artistOfTheDay + "." : "has not been set yet.")); }, help: function (target, room) { if (room.id !== 'thestudio') return this.errorReply('This command can only be used in The Studio.'); - if (!room.chatRoomData || !this.canBroadcast()) return false; + if (!room.chatRoomData || !this.runBroadcast()) return false; this.sendReply("Use /help aotd to view help for all commands, or /help aotd [command] for help on a specific command."); }, }; diff --git a/chat-plugins/trivia.js b/chat-plugins/trivia.js index f16d21b6fc..0793c38e17 100644 --- a/chat-plugins/trivia.js +++ b/chat-plugins/trivia.js @@ -822,7 +822,7 @@ let commands = { let buffer = "|raw|
"; if (!target) { - if (!this.canBroadcast()) return false; + if (!this.runBroadcast()) return false; let questions = triviaData.questions; let questionsLen = questions.length; @@ -882,7 +882,7 @@ let commands = { '': 'status', status: function (target, room, user) { if (room.id !== 'trivia') return this.errorReply('This command can only be used in Trivia.'); - if (!this.canBroadcast()) return false; + if (!this.runBroadcast()) return false; let trivium = trivia[room.id]; if (!trivium) return this.errorReply("There is no trivia game in progress."); trivium.getStatus(this, user); @@ -891,7 +891,7 @@ let commands = { players: function (target, room) { if (room.id !== 'trivia') return this.errorReply('This command can only be used in Trivia.'); - if (!this.canBroadcast()) return false; + if (!this.runBroadcast()) return false; let trivium = trivia[room.id]; if (!trivium) return this.errorReply("There is no trivia game in progress."); trivium.getParticipants(this); @@ -926,7 +926,7 @@ let commands = { ladder: function (target, room) { if (room.id !== 'trivia') return this.errorReply('This command can only be used in Trivia.'); - if (!this.canBroadcast()) return false; + if (!this.runBroadcast()) return false; let ladder = triviaData.ladder; let leaderboard = triviaData.leaderboard; diff --git a/chat-plugins/wifi.js b/chat-plugins/wifi.js index 09a9263772..6848ebf738 100644 --- a/chat-plugins/wifi.js +++ b/chat-plugins/wifi.js @@ -361,7 +361,7 @@ let commands = { if (room.id !== 'wifi') return this.errorReply("This command can only be used in the Wi-Fi room."); let giveaway = giveaways[room.id]; if (!giveaway) return this.errorReply("There is no giveaway going on at the moment."); - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (giveaway.type === 'question') { if (giveaway.phase !== 'started') return this.errorReply("The giveaway has not started yet."); this.sendReply("|html|
Question Giveaway started by " + Tools.escapeHTML(giveaway.host.name) + "
" + @@ -390,7 +390,7 @@ let commands = { case 'game': case 'giveaway': case 'user': - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; reply = 'Giveaway participation commands: (start with /giveaway, except for /ga)
' + '- guess or /ga answer - Guesses the answer for a question giveaway
' + '- viewanswer - Shows the answer in a question giveaway (only to host/giver)
' + @@ -399,7 +399,7 @@ let commands = { '- leave or leavelottery - Leaves a lottery giveaway
'; break; default: - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; reply = 'Wi-Fi room Giveaway help and info
' + '- help user - shows list of participation commands
' + '- help staff - shows giveaway staff commands (Requires: % @ # & ~)'; diff --git a/command-parser.js b/command-parser.js index fb4128ce92..4efafa36aa 100644 --- a/command-parser.js +++ b/command-parser.js @@ -170,31 +170,54 @@ class CommandContext { } return true; } - canBroadcast(checkOnly, suppressMessage) { - let message = this.canTalk(this.message); - if (!message) return false; - let normalized = message.toLowerCase().replace(/[^a-z0-9\s!,]/g, ''); - + canBroadcast() { if (!this.broadcasting && this.cmdToken === BROADCAST_TOKEN) { + if (this.user.broadcasting) { + this.errorReply("You can't broadcast another command too soon."); + return false; + } + + let message = this.canTalk(this.message); + if (!message) return false; if (!this.user.can('broadcast', null, this.room)) { this.errorReply("You need to be voiced to broadcast this command's information."); - this.errorReply("To see it for yourself, use: /" + message.substr(1)); + this.errorReply("To see it for yourself, use: /" + this.message.substr(1)); return false; } // broadcast cooldown - if (this.room.lastBroadcast === normalized && + let broadcastMessage = message.toLowerCase().replace(/[^a-z0-9\s!,]/g, ''); + + if (this.room.lastBroadcast === this.broadcastMessage && this.room.lastBroadcastTime >= Date.now() - BROADCAST_COOLDOWN) { this.errorReply("You can't broadcast this because it was just broadcast."); return false; } - this.broadcasting = true; + + this.message = message; + this.broadcastMessage = broadcastMessage; + this.user.broadcasting = true; } - if (this.broadcasting && !checkOnly) { - this.add('|c|' + this.user.getIdentity(this.room.id) + '|' + (suppressMessage || message)); - this.room.lastBroadcast = normalized; - this.room.lastBroadcastTime = Date.now(); + return true; + } + runBroadcast(suppressMessage) { + if (this.broadcasting || this.cmdToken !== BROADCAST_TOKEN) { + // Already being broadcast, or the user doesn't intend to broadcast. + return true; } + + if (!this.broadcastMessage) { + // Permission hasn't been checked yet. Do it now. + if (!this.canBroadcast()) return false; + } + + this.add('|c|' + this.user.getIdentity(this.room.id) + '|' + (suppressMessage || this.message)); + this.room.lastBroadcast = this.broadcastMessage; + this.room.lastBroadcastTime = Date.now(); + + this.broadcasting = true; + this.user.broadcasting = false; + return true; } parse(message, inNamespace, room) { diff --git a/commands.js b/commands.js index cd001e24f6..a3d89a3014 100644 --- a/commands.js +++ b/commands.js @@ -25,7 +25,7 @@ const HOURMUTE_LENGTH = 60 * 60 * 1000; exports.commands = { version: function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReplyBox("Server version: " + CommandParser.package.version + ""); }, @@ -594,7 +594,7 @@ exports.commands = { roomdesc: function (target, room, user) { if (!target) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (!room.desc) return this.sendReply("This room does not have a description set."); this.sendReplyBox("The room description is: " + Tools.escapeHTML(room.desc)); return; @@ -627,7 +627,7 @@ exports.commands = { topic: 'roomintro', roomintro: function (target, room, user) { if (!target) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (!room.introMessage) return this.sendReply("This room does not have an introduction set."); this.sendReply('|raw|
' + room.introMessage + '
'); if (!this.broadcasting && user.can('declare', null, room)) { @@ -696,7 +696,7 @@ exports.commands = { roomalias: function (target, room, user) { if (!target) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (!room.aliases || !room.aliases.length) return this.sendReplyBox("This room does not have any aliases."); return this.sendReplyBox("This room has the following aliases: " + room.aliases.join(", ") + ""); } @@ -2241,7 +2241,7 @@ exports.commands = { if (!user.hasConsoleAccess(connection)) { return this.errorReply("/eval - Access denied."); } - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (!this.broadcasting) this.sendReply('||>> ' + target); try { @@ -2259,7 +2259,7 @@ exports.commands = { if (!user.hasConsoleAccess(connection)) { return this.errorReply("/evalbattle - Access denied."); } - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; if (!room.battle) { return this.errorReply("/evalbattle - This isn't a battle room."); } diff --git a/tournaments/index.js b/tournaments/index.js index f761feb964..6a2e7b43b3 100644 --- a/tournaments/index.js +++ b/tournaments/index.js @@ -806,7 +806,7 @@ let commands = { } }, getusers: function (tournament) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; let users = usersToNames(tournament.generator.getUsers(true).sort()); this.sendReplyBox("" + users.length + " users remain in this tournament:
" + Tools.escapeHTML(users.join(", "))); }, @@ -993,7 +993,7 @@ CommandParser.commands.tournament = function (paramString, room, user) { if (!params[0]) params = []; if (cmd === '') { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; this.sendReply('|tournaments|info|' + JSON.stringify(Object.keys(exports.tournaments).filter(tournament => { tournament = exports.tournaments[tournament]; return !tournament.room.isPrivate && !tournament.room.isPersonal && !tournament.room.staffRoom; @@ -1112,7 +1112,7 @@ CommandParser.commands.tournament = function (paramString, room, user) { } }; CommandParser.commands.tournamenthelp = function (target, room, user) { - if (!this.canBroadcast()) return; + if (!this.runBroadcast()) return; return this.sendReplyBox( "- create/new <format>, <type> [, <comma-separated arguments>]: Creates a new tournament in the current room.
" + "- settype <type> [, <comma-separated arguments>]: Modifies the type of tournament after it's been created, but before it has started.
" + diff --git a/users.js b/users.js index 90c5614e49..2e241ed0c3 100644 --- a/users.js +++ b/users.js @@ -431,6 +431,7 @@ class User { this.chatQueue = null; this.chatQueueTimeout = null; this.lastChatMessage = 0; + this.broadcasting = false; // for the anti-spamming mechanism this.lastMessage = '';