diff --git a/modules/cmdmgr.pike b/modules/cmdmgr.pike index 8cde06ff..26b85763 100644 --- a/modules/cmdmgr.pike +++ b/modules/cmdmgr.pike @@ -56,6 +56,269 @@ void autospam(string channel, string msg) { } } +//Map a flag name to a set of valid values for it +//Blank or null is always allowed, and will result in no flag being set. +constant message_flags = ([ + "mode": (<"random", "rotate", "foreach">), + "dest": (<"/w", "/web", "/set", "/chain", "/reply", "//">), +]); +//As above, but applying only to the top level of a command. +constant command_flags = ([ + "access": (<"mod", "vip", "none">), + "visibility": (<"hidden">), +]); + +constant condition_parts = ([ + "string": ({"expr1", "expr2", "casefold"}), + "contains": ({"expr1", "expr2", "casefold"}), + "regexp": ({"expr1", "expr2", "casefold"}), + "number": ({"expr1"}), //Yes, expr1 even though there's no others - means you still see it when you switch (in the classic editor) + "spend": ({"expr1", "expr2"}), //Similarly, this uses the same names for the sake of the classic editor's switching. + "cooldown": ({"cdname", "cdlength", "cdqueue"}), + "catch": ({ }), //Currently there's no exception type hierarchy, so you always catch everything. +]); + +string normalize_cooldown_name(string|int(0..0) cdname, mapping state) { + sscanf(cdname || "", "%[*]%s", string per_user, string name); + //Anonymous cooldowns get named for the back end, but the front end will blank this. + //If the front end happens to return something with a dot name in it, ignore it. + if (name == "" || name[0] == '.') name = sprintf(".%s:%d", state->cmd, ++state->cdanon); + return per_user + name; +} + +//state array is for purely-linear state that continues past subtrees +echoable_message _validate_recursive(echoable_message resp, mapping state) +{ + //Filter the response to only that which is valid + if (stringp(resp)) return resp; + if (arrayp(resp)) switch (sizeof(resp)) + { + case 0: return ""; //This should be dealt with at a higher level (and suppressed). + case 1: return _validate_recursive(resp[0], state); //Collapse single element arrays to their sole element + default: return _validate_recursive(resp[*], state) - ({""}); //Suppress any empty entries + } + if (!mappingp(resp)) return ""; //Ensure that nulls become empty strings, for safety and UI simplicity. + mapping ret = (["message": _validate_recursive(resp->message, state)]); + //Whitelist the valid flags. Note that this will quietly suppress any empty + //strings, which would be stating the default behaviour. + foreach (message_flags; string flag; multiset ok) + { + if (ok[resp[flag]]) ret[flag] = resp[flag]; + } + if (ret->dest == "//") { + //Comments begin with a double slash. Whodathunk? + //They're not allowed to have anything else though, just the message. + //The message itself won't be processed in any way, and could actually + //contain other, more complex, content, but as long as it's syntactically + //valid, nothing will be done with it. + return ret & (<"dest", "message">); + } + if (ret->dest) { + //If there's any dest other than "" (aka "open chat") or "//", it should + //have a target. Failing to have a target breaks other destinations, + //so remove that if this is missing; otherwise, any target works. + if (!resp->target) m_delete(ret, "dest"); + else ret->target = resp->target; + if (ret->dest == "/chain") { + //Command chaining gets extra validation done. You may ONLY chain to + //commands from the current channel; but you may enter them with + //or without their leading exclamation marks. + string cmd = (ret->target || "") - "!"; + if (state->channel && !state->channel->commands[cmd]) + //Attempting to chain to something that doesn't exist is invalid. + //TODO: Accept it if it's recursion (or maybe have a separate "chain + //to self" notation) to allow a new recursive command to be saved. + return ""; + ret->target = cmd; + } + //Variable names containing these characters would be unable to be correctly output + //in any command, due to the way variable substitution is processed. + if (ret->dest == "/set") ret->target = replace(ret->target, "|${}" / 1, ""); + } + if (resp->dest == "/builtin" && resp->target) { + //A dest of "/builtin" is really a builtin. What a surprise :) + sscanf(resp->target, "!%[^ ]%*[ ]%s", resp->builtin, resp->builtin_param); + } + else if (resp->dest && has_prefix(resp->dest, "/")) + { + //Legacy mode. Fracture the dest into dest and target. + sscanf(resp->dest, "/%[a-z] %[a-zA-Z$%]%s", string dest, string target, string empty); + if ((<"w", "web", "set">)[dest] && target != "" && empty == "") + [ret->dest, ret->target] = ({"/" + dest, target}); + //NOTE: In theory, a /web message's destcfg could represent an entire message subtree. + //Currently only simple strings will pass validation though. + //Note also that not all destcfgs are truly meaningful, but any string is valid and + //will be saved. + if (stringp(resp->destcfg) && resp->destcfg != "") ret->destcfg = resp->destcfg; + else if (resp->action == "add") ret->destcfg = "add"; //Handle variable management in the old style + } + if (resp->builtin && G->G->builtins[resp->builtin]) { + //Validated separately as the builtins aren't a constant + ret->builtin = resp->builtin; + //Simple string? Let the builtin itself handle it. + if (stringp(resp->builtin_param) && resp->builtin_param != "") ret->builtin_param = resp->builtin_param; + //Array of strings? Maybe we should validate the number of arguments (different per builtin), + //but for now, any array of strings will be accepted. + else if (arrayp(resp->builtin_param) && sizeof(resp->builtin_param) + && !has_value(stringp(resp->builtin_param[*]), 0)) + ret->builtin_param = resp->builtin_param; + } + //Conditions have their own active ingredients. + if (array parts = condition_parts[resp->conditional]) { + foreach (parts + ({"conditional"}), string key) + if (resp[key]) ret[key] = resp[key]; + ret->otherwise = _validate_recursive(resp->otherwise, state); + if (ret->message == "" && ret->otherwise == "") return ""; //Conditionals can omit either message or otherwise, but not both + if (ret->casefold == "") m_delete(ret, "casefold"); //Blank means not case folded, so omit it + if (ret->conditional == "cooldown") { + ret->cdname = normalize_cooldown_name(ret->cdname, state); + ret->cdlength = (int)ret->cdlength; + if (ret->cdlength) state->cooldowns[ret->cdname] = ret->cdlength; + else m_delete(ret, (({"conditional", "otherwise"}) + parts)[*]); //Not a valid cooldown. + //TODO: Keyword-synchronized cooldowns should synchronize their cdlengths too + } + } + else if (ret->message == "" && (<0, "/web", "/w", "/reply">)[ret->dest] && !ret->builtin) { + //No message? Nothing to do, if a standard destination. Destinations like + //"set variable" are perfectly happy to accept blank messages, and builtins + //can be used for their side effects only. Note that it's up to the command + //designer to know whether this is meaningful or not (Arg Split with no + //content isn't very helpful, but Log absolutely would be). + return ""; + } + //Delays are integer seconds. We'll permit a string of digits, since that might be + //easier for the front end. + if (resp->delay && resp->delay != "0" && + (intp(resp->delay) || (sscanf((string)resp->delay, "%[0-9]", string d) && d == resp->delay))) + ret->delay = (int)resp->delay; + + if (ret->mode == "rotate") { + //Anonymous rotations, like anonymous cooldowns, get named for the back end only. + //In this case, though, it also creates a variable. For simplicity, reuse cdanon. + ret->rotatename = normalize_cooldown_name(resp->rotatename, state); + } + //Iteration can be done on all-in-chat or all-who've-chatted. + if (int timeout = ret->mode == "foreach" && (int)resp->participant_activity) + ret->participant_activity = timeout; + + //Voice ID validity depends on the channel we're working with. A syntax-only check will + //accept any voice ID as long as it's a string of digits. + if (!state->channel) { + if (resp->voice && sscanf(resp->voice, "%[0-9]%s", string v, string end) && v != "" && end == "") ret->voice = v; + } + else if ((state->channel->config->voices || ([]))[resp->voice]) ret->voice = resp->voice; + //Setting voice to "0" resets to the global default, which is useful if there's a local default. + else if (resp->voice == "0" && state->channel->config->defvoice) ret->voice = resp->voice; + else if (resp->voice == "") { + //Setting voice to blank means "use channel default". This is useful if, + //and only if, you've already set it to a nondefault voice in this tree. + //TODO: Track changes to voices and allow such a reset to default. + } + + if (sizeof(ret) == 1) return ret->message; //No flags? Just return the message. + return ret; +} +echoable_message _validate_toplevel(echoable_message resp, mapping state) +{ + mixed ret = _validate_recursive(resp, state); + if (!mappingp(resp)) return ret; //There can't be any top-level flags if you start with a string or array + if (!mappingp(ret)) ret = (["message": ret]); + //If there are any top-level flags, apply them. + //TODO: Only do this for commands, not specials or triggers. + foreach (command_flags; string flag; multiset ok) + { + if (ok[resp[flag]]) ret[flag] = resp[flag]; + } + + //Aliases are blank-separated, and might be entered in the UI with bangs. + //But internally, we'd rather have them without. (Also, trim off any junk.) + array(string) aliases = (resp->aliases || "") / " "; + foreach (aliases; int i; string a) sscanf(a, "%*[!]%s%*[#\n]", aliases[i]); + aliases -= ({"", state->cmd}); //Disallow blank, or an alias pointing back to self (it'd be ignored anyway) + if (sizeof(aliases)) ret->aliases = command_casefold(aliases * " "); + + //Automation comes in a couple of strict forms; anything else gets dropped. + //Very very basic validation is done (no zero-minute automation) but otherwise, stupid stuff is + //fine; I'm not going to stop you from setting a command to run every 1048576 minutes. + if (stringp(resp->automate)) { + if (sscanf(resp->automate, "%d:%d", int hr, int min) == 2) ret->automate = ({hr, min, 1}); + else if (sscanf(resp->automate, "%d-%d", int min, int max) && min >= 0 && max >= min && max > 0) ret->automate = ({min, max, 0}); + else if (sscanf(resp->automate, "%d", int minmax) && minmax > 0) ret->automate = ({minmax, minmax, 0}); + //Else don't set ret->automate. + } else if (arrayp(resp->automate) && sizeof(resp->automate) == 3 && min(@resp->automate) >= 0 && max(@resp->automate) > 0 && resp->automate[2] <= 1) + ret->automate = resp->automate; + + //TODO: Ensure that the reward still exists + if (stringp(resp->redemption) && resp->redemption != "") ret->redemption = resp->redemption; + + return sizeof(ret) == 1 ? ret->message : ret; +} + +//mode is "" for regular commands, "!!" for specials, "!!trigger" for triggers. +array validate_command(object channel, string|zero mode, string cmdname, echoable_message response, string|void original) { + mapping state = (["cdanon": 0, "cooldowns": ([]), "channel": channel]); + switch (mode) { + case "!!trigger": { + echoable_message alltrig = channel->commands["!trigger"]; + alltrig += ({ }); //Force array, and disconnect it for mutation's sake + string id = cmdname - "!"; + if (id == "") { + //Blank command name? Create a new one. + if (!sizeof(alltrig)) id = "1"; + else id = (string)((int)alltrig[-1]->id + 1); + } + else if (id == "validateme" || has_prefix(id, "changetab_")) + return ({0, _validate_toplevel(response, state)}); //Validate-only and ignore preexisting triggers + else if (!(int)id) return 0; //Invalid ID + state->cmd = "!!trigger-" + id; + echoable_message trigger = _validate_toplevel(response, state); + if (trigger != "") { //Empty string will cause a deletion + if (!mappingp(trigger)) trigger = (["message": trigger]); + trigger->id = id; + m_delete(trigger, "otherwise"); //Triggers don't have an Else clause + } + if (cmdname == "") alltrig += ({trigger}); + else foreach ([array]alltrig; int i; mapping r) { + if (r->id == id) { + alltrig[i] = trigger; + break; + } + } + alltrig -= ({""}); + if (!sizeof(alltrig)) alltrig = ""; //No triggers left? Delete the special altogether. + return ({"!trigger" + channel->name, alltrig, state}); + } + case "": case "!!": { + string pfx = mode[..0]; //"!" for specials, "" for normals + if (!stringp(cmdname)) return 0; + sscanf(cmdname, "%*[!]%s%*[#]%s", string|zero command, string c); + if (c != "" && c != channel->name[1..]) return 0; //If you specify the command name as "!demo#rosuav", that's fine if and only if you're working with channel "#rosuav". + command = String.trim(lower_case(command)); + if (command == "") return 0; + state->cmd = command = pfx + command; + if (pfx == "!" && !function_object(make_echocommand)->SPECIAL_NAMES[command]) command = 0; //Only specific specials are valid + if (pfx == "") { + //See if an original name was provided + sscanf(original || "", "%*[!]%s%*[#]", string orig); + orig = String.trim(lower_case(orig)); + if (orig != "") state->original = orig + channel->name; + } + //Validate the message. Note that there will be some things not caught by this + //(eg trying to set access or visibility deep within the response), but they + //will be merely useless, not problematic. + return ({command + channel->name, _validate_toplevel(response, state), state}); + } + default: return 0; //Internal error, shouldn't happen + } +} + +//Validate and update. TODO: Move make_echocommand into here as _save_echocommand, and +//use this helper for all external updates. +void update_command(object channel, string command, string cmdname, echoable_message response, string|void original) { + array valid = validate_command(channel, command, cmdname, response, original); + if (valid) make_echocommand(@valid); +} + constant builtin_description = "Manage channel commands"; constant builtin_name = "Command manager"; constant builtin_param = ({"/Action/Automate/Create/Delete", "Command name", "Time/message"}); @@ -189,6 +452,8 @@ void scan_command(mapping state, echoable_message message) { protected void create(string name) { ::create(name); + G->G->cmdmgr = this; + G->G->update_command = update_command; //Deprecated alias for G->G->cmdmgr->update_command register_bouncer(autospam); foreach (list_channel_configs(), mapping cfg) if (cfg->login) if (G->G->stream_online_since[cfg->login]) connected(cfg->login); diff --git a/modules/http/chan_commands.pike b/modules/http/chan_commands.pike index d51d3cf3..0ff38708 100644 --- a/modules/http/chan_commands.pike +++ b/modules/http/chan_commands.pike @@ -66,7 +66,7 @@ void enable_feature(object channel, string kwd, int state) { if (!info) foreach (G->G->builtins; string name; object blt) if (blt->command_suggestions[?kwd]) info = (["response": blt->command_suggestions[kwd]]); if (!info) return; - make_echocommand(@_validate_command(channel, "", kwd, state ? info->response || COMPLEX_TEMPLATES[kwd] : "")); + G->G->cmdmgr->update_command(channel, "", kwd, state ? info->response || COMPLEX_TEMPLATES[kwd] : ""); } //Gather all the variables that the JS command editor needs. Some may depend on the channel. @@ -116,7 +116,8 @@ mapping(string:mixed) http_request(Protocols.HTTP.Server.Request req) //or a zero. mixed body = Standards.JSON.decode(utf8_to_string(req->body_raw)); if (!body || !mappingp(body) || !mappingp(body->msg)) return (["error": 400]); - echoable_message result = _validate_toplevel(body->msg, (["cmd": body->cmdname || "validateme", "cooldowns": ([])])); + //NOTE: Uses an internal API. So this is undocumented AND unsupported. Great. + echoable_message result = G->G->cmdmgr->_validate_toplevel(body->msg, (["cmd": body->cmdname || "validateme", "cooldowns": ([])])); if (body->cmdname == "!!trigger" && result != "") { if (!mappingp(result)) result = (["message": result]); m_delete(result, "otherwise"); //Triggers don't have an Else clause @@ -200,273 +201,8 @@ mapping get_state(string group, string|void id) { return (["items": commands]); } -//Map a flag name to a set of valid values for it -//Blank or null is always allowed, and will result in no flag being set. -constant message_flags = ([ - "mode": (<"random", "rotate", "foreach">), - "dest": (<"/w", "/web", "/set", "/chain", "/reply", "//">), -]); -//As above, but applying only to the top level of a command. -constant command_flags = ([ - "access": (<"mod", "vip", "none">), - "visibility": (<"hidden">), -]); - -constant condition_parts = ([ - "string": ({"expr1", "expr2", "casefold"}), - "contains": ({"expr1", "expr2", "casefold"}), - "regexp": ({"expr1", "expr2", "casefold"}), - "number": ({"expr1"}), //Yes, expr1 even though there's no others - means you still see it when you switch (in the classic editor) - "spend": ({"expr1", "expr2"}), //Similarly, this uses the same names for the sake of the classic editor's switching. - "cooldown": ({"cdname", "cdlength", "cdqueue"}), - "catch": ({ }), //Currently there's no exception type hierarchy, so you always catch everything. -]); - -string normalize_cooldown_name(string|int(0..0) cdname, mapping state) { - sscanf(cdname || "", "%[*]%s", string per_user, string name); - //Anonymous cooldowns get named for the back end, but the front end will blank this. - //If the front end happens to return something with a dot name in it, ignore it. - if (name == "" || name[0] == '.') name = sprintf(".%s:%d", state->cmd, ++state->cdanon); - return per_user + name; -} - -//state array is for purely-linear state that continues past subtrees -echoable_message _validate_recursive(echoable_message resp, mapping state) -{ - //Filter the response to only that which is valid - if (stringp(resp)) return resp; - if (arrayp(resp)) switch (sizeof(resp)) - { - case 0: return ""; //This should be dealt with at a higher level (and suppressed). - case 1: return _validate_recursive(resp[0], state); //Collapse single element arrays to their sole element - default: return _validate_recursive(resp[*], state) - ({""}); //Suppress any empty entries - } - if (!mappingp(resp)) return ""; //Ensure that nulls become empty strings, for safety and UI simplicity. - mapping ret = (["message": _validate_recursive(resp->message, state)]); - //Whitelist the valid flags. Note that this will quietly suppress any empty - //strings, which would be stating the default behaviour. - foreach (message_flags; string flag; multiset ok) - { - if (ok[resp[flag]]) ret[flag] = resp[flag]; - } - if (ret->dest == "//") { - //Comments begin with a double slash. Whodathunk? - //They're not allowed to have anything else though, just the message. - //The message itself won't be processed in any way, and could actually - //contain other, more complex, content, but as long as it's syntactically - //valid, nothing will be done with it. - return ret & (<"dest", "message">); - } - if (ret->dest) { - //If there's any dest other than "" (aka "open chat") or "//", it should - //have a target. Failing to have a target breaks other destinations, - //so remove that if this is missing; otherwise, any target works. - if (!resp->target) m_delete(ret, "dest"); - else ret->target = resp->target; - if (ret->dest == "/chain") { - //Command chaining gets extra validation done. You may ONLY chain to - //commands from the current channel; but you may enter them with - //or without their leading exclamation marks. - string cmd = (ret->target || "") - "!"; - if (state->channel && !state->channel->commands[cmd]) - //Attempting to chain to something that doesn't exist is invalid. - //TODO: Accept it if it's recursion (or maybe have a separate "chain - //to self" notation) to allow a new recursive command to be saved. - return ""; - ret->target = cmd; - } - //Variable names containing these characters would be unable to be correctly output - //in any command, due to the way variable substitution is processed. - if (ret->dest == "/set") ret->target = replace(ret->target, "|${}" / 1, ""); - } - if (resp->dest == "/builtin" && resp->target) { - //A dest of "/builtin" is really a builtin. What a surprise :) - sscanf(resp->target, "!%[^ ]%*[ ]%s", resp->builtin, resp->builtin_param); - } - else if (resp->dest && has_prefix(resp->dest, "/")) - { - //Legacy mode. Fracture the dest into dest and target. - sscanf(resp->dest, "/%[a-z] %[a-zA-Z$%]%s", string dest, string target, string empty); - if ((<"w", "web", "set">)[dest] && target != "" && empty == "") - [ret->dest, ret->target] = ({"/" + dest, target}); - //NOTE: In theory, a /web message's destcfg could represent an entire message subtree. - //Currently only simple strings will pass validation though. - //Note also that not all destcfgs are truly meaningful, but any string is valid and - //will be saved. - if (stringp(resp->destcfg) && resp->destcfg != "") ret->destcfg = resp->destcfg; - else if (resp->action == "add") ret->destcfg = "add"; //Handle variable management in the old style - } - if (resp->builtin && G->G->builtins[resp->builtin]) { - //Validated separately as the builtins aren't a constant - ret->builtin = resp->builtin; - //Simple string? Let the builtin itself handle it. - if (stringp(resp->builtin_param) && resp->builtin_param != "") ret->builtin_param = resp->builtin_param; - //Array of strings? Maybe we should validate the number of arguments (different per builtin), - //but for now, any array of strings will be accepted. - else if (arrayp(resp->builtin_param) && sizeof(resp->builtin_param) - && !has_value(stringp(resp->builtin_param[*]), 0)) - ret->builtin_param = resp->builtin_param; - } - //Conditions have their own active ingredients. - if (array parts = condition_parts[resp->conditional]) { - foreach (parts + ({"conditional"}), string key) - if (resp[key]) ret[key] = resp[key]; - ret->otherwise = _validate_recursive(resp->otherwise, state); - if (ret->message == "" && ret->otherwise == "") return ""; //Conditionals can omit either message or otherwise, but not both - if (ret->casefold == "") m_delete(ret, "casefold"); //Blank means not case folded, so omit it - if (ret->conditional == "cooldown") { - ret->cdname = normalize_cooldown_name(ret->cdname, state); - ret->cdlength = (int)ret->cdlength; - if (ret->cdlength) state->cooldowns[ret->cdname] = ret->cdlength; - else m_delete(ret, (({"conditional", "otherwise"}) + parts)[*]); //Not a valid cooldown. - //TODO: Keyword-synchronized cooldowns should synchronize their cdlengths too - } - } - else if (ret->message == "" && (<0, "/web", "/w", "/reply">)[ret->dest] && !ret->builtin) { - //No message? Nothing to do, if a standard destination. Destinations like - //"set variable" are perfectly happy to accept blank messages, and builtins - //can be used for their side effects only. Note that it's up to the command - //designer to know whether this is meaningful or not (Arg Split with no - //content isn't very helpful, but Log absolutely would be). - return ""; - } - //Delays are integer seconds. We'll permit a string of digits, since that might be - //easier for the front end. - if (resp->delay && resp->delay != "0" && - (intp(resp->delay) || (sscanf((string)resp->delay, "%[0-9]", string d) && d == resp->delay))) - ret->delay = (int)resp->delay; - - if (ret->mode == "rotate") { - //Anonymous rotations, like anonymous cooldowns, get named for the back end only. - //In this case, though, it also creates a variable. For simplicity, reuse cdanon. - ret->rotatename = normalize_cooldown_name(resp->rotatename, state); - } - //Iteration can be done on all-in-chat or all-who've-chatted. - if (int timeout = ret->mode == "foreach" && (int)resp->participant_activity) - ret->participant_activity = timeout; - - //Voice ID validity depends on the channel we're working with. A syntax-only check will - //accept any voice ID as long as it's a string of digits. - if (!state->channel) { - if (resp->voice && sscanf(resp->voice, "%[0-9]%s", string v, string end) && v != "" && end == "") ret->voice = v; - } - else if ((state->channel->config->voices || ([]))[resp->voice]) ret->voice = resp->voice; - //Setting voice to "0" resets to the global default, which is useful if there's a local default. - else if (resp->voice == "0" && state->channel->config->defvoice) ret->voice = resp->voice; - else if (resp->voice == "") { - //Setting voice to blank means "use channel default". This is useful if, - //and only if, you've already set it to a nondefault voice in this tree. - //TODO: Track changes to voices and allow such a reset to default. - } - - if (sizeof(ret) == 1) return ret->message; //No flags? Just return the message. - return ret; -} -echoable_message _validate_toplevel(echoable_message resp, mapping state) -{ - mixed ret = _validate_recursive(resp, state); - if (!mappingp(resp)) return ret; //There can't be any top-level flags if you start with a string or array - if (!mappingp(ret)) ret = (["message": ret]); - //If there are any top-level flags, apply them. - //TODO: Only do this for commands, not specials or triggers. - foreach (command_flags; string flag; multiset ok) - { - if (ok[resp[flag]]) ret[flag] = resp[flag]; - } - - //Aliases are blank-separated, and might be entered in the UI with bangs. - //But internally, we'd rather have them without. (Also, trim off any junk.) - array(string) aliases = (resp->aliases || "") / " "; - foreach (aliases; int i; string a) sscanf(a, "%*[!]%s%*[#\n]", aliases[i]); - aliases -= ({"", state->cmd}); //Disallow blank, or an alias pointing back to self (it'd be ignored anyway) - if (sizeof(aliases)) ret->aliases = command_casefold(aliases * " "); - - //Automation comes in a couple of strict forms; anything else gets dropped. - //Very very basic validation is done (no zero-minute automation) but otherwise, stupid stuff is - //fine; I'm not going to stop you from setting a command to run every 1048576 minutes. - if (stringp(resp->automate)) { - if (sscanf(resp->automate, "%d:%d", int hr, int min) == 2) ret->automate = ({hr, min, 1}); - else if (sscanf(resp->automate, "%d-%d", int min, int max) && min >= 0 && max >= min && max > 0) ret->automate = ({min, max, 0}); - else if (sscanf(resp->automate, "%d", int minmax) && minmax > 0) ret->automate = ({minmax, minmax, 0}); - //Else don't set ret->automate. - } else if (arrayp(resp->automate) && sizeof(resp->automate) == 3 && min(@resp->automate) >= 0 && max(@resp->automate) > 0 && resp->automate[2] <= 1) - ret->automate = resp->automate; - - //TODO: Ensure that the reward still exists - if (stringp(resp->redemption) && resp->redemption != "") ret->redemption = resp->redemption; - - return sizeof(ret) == 1 ? ret->message : ret; -} - -//mode is "" for regular commands, "!!" for specials, "!!trigger" for triggers. -array _validate_command(object channel, string|zero mode, string cmdname, echoable_message response, string|void original) { - mapping state = (["cdanon": 0, "cooldowns": ([]), "channel": channel]); - switch (mode) { - case "!!trigger": { - echoable_message alltrig = channel->commands["!trigger"]; - alltrig += ({ }); //Force array, and disconnect it for mutation's sake - string id = cmdname - "!"; - if (id == "") { - //Blank command name? Create a new one. - if (!sizeof(alltrig)) id = "1"; - else id = (string)((int)alltrig[-1]->id + 1); - } - else if (id == "validateme" || has_prefix(id, "changetab_")) - return ({0, _validate_toplevel(response, state)}); //Validate-only and ignore preexisting triggers - else if (!(int)id) return 0; //Invalid ID - state->cmd = "!!trigger-" + id; - echoable_message trigger = _validate_toplevel(response, state); - if (trigger != "") { //Empty string will cause a deletion - if (!mappingp(trigger)) trigger = (["message": trigger]); - trigger->id = id; - m_delete(trigger, "otherwise"); //Triggers don't have an Else clause - } - if (cmdname == "") alltrig += ({trigger}); - else foreach ([array]alltrig; int i; mapping r) { - if (r->id == id) { - alltrig[i] = trigger; - break; - } - } - alltrig -= ({""}); - if (!sizeof(alltrig)) alltrig = ""; //No triggers left? Delete the special altogether. - return ({"!trigger" + channel->name, alltrig, state}); - } - case "": case "!!": { - string pfx = mode[..0]; //"!" for specials, "" for normals - if (!stringp(cmdname)) return 0; - sscanf(cmdname, "%*[!]%s%*[#]%s", string|zero command, string c); - if (c != "" && c != channel->name[1..]) return 0; //If you specify the command name as "!demo#rosuav", that's fine if and only if you're working with channel "#rosuav". - command = String.trim(lower_case(command)); - if (command == "") return 0; - state->cmd = command = pfx + command; - if (pfx == "!" && !function_object(make_echocommand)->SPECIAL_NAMES[command]) command = 0; //Only specific specials are valid - if (pfx == "") { - //See if an original name was provided - sscanf(original || "", "%*[!]%s%*[#]", string orig); - orig = String.trim(lower_case(orig)); - if (orig != "") state->original = orig + channel->name; - } - //Validate the message. Note that there will be some things not caught by this - //(eg trying to set access or visibility deep within the response), but they - //will be merely useless, not problematic. - return ({command + channel->name, _validate_toplevel(response, state), state}); - } - default: return 0; //Internal error, shouldn't happen - } -} - -//TODO: Move this out of chan_commands along with everything it depends on. -//This function should not depend on anything web or websocket-specific. -//TODO: Remove the original parameter and have anything capable of renaming -//add it directly to state, so addcmd can purge old versions of a command. -void update_command(object channel, string command, string cmdname, echoable_message response, string|void original) { - array valid = _validate_command(channel, command, cmdname, response, original); - if (valid && valid[1] != "") make_echocommand(@valid); -} - void wscmd_update(object channel, mapping(string:mixed) conn, mapping(string:mixed) msg) { - array valid = _validate_command(channel, (conn->group / "#")[0], msg->cmdname, msg->response, msg->original); + array valid = G->G->cmdmgr->validate_command(channel, (conn->group / "#")[0], msg->cmdname, msg->response, msg->original); if (!valid || !valid[0]) return; make_echocommand(@valid); if (msg->cmdname == "" && has_prefix(conn->group, "!!trigger#")) { @@ -479,7 +215,7 @@ void wscmd_delete(object c, mapping conn, mapping msg) {wscmd_update(c, conn, ms void websocket_cmd_validate(mapping(string:mixed) conn, mapping(string:mixed) msg) { sscanf(conn->group, "%s#%s", string mode, string chan); object channel = G->G->irc->channels["#" + chan]; if (!channel) return 0; - array valid = _validate_command(channel, mode, msg->cmdname || "validateme", msg->response, msg->original); + array valid = G->G->cmdmgr->validate_command(channel, mode, msg->cmdname || "validateme", msg->response, msg->original); if (!valid) return; //But it's okay if the name is invalid, or in demo mode (fake-mod) string cmdname = ((valid[0] || msg->cmdname || "validateme") / "#")[0]; conn->sock->send_text(Standards.JSON.encode((["cmd": "validated", "cmdname": cmdname, "response": valid[1]]), 4)); @@ -488,7 +224,6 @@ void websocket_cmd_validate(mapping(string:mixed) conn, mapping(string:mixed) ms protected void create(string name) { ::create(name); call_out(find_builtins, 0); - G->G->update_command = update_command; //TODO: Migrate this into cmdmgr.pike //Final layer of migration: Remove any feature mapping references, which are now ignored. foreach (list_channel_configs(), mapping cfg) if (m_delete(cfg, "features")) persist_config->save();