def match_role(var, role: str, remove_spaces: bool = False, allow_extra: bool = False, allow_special: bool = True, scope: Optional[Iterable[str]] = None) -> Match[LocalRole]: """ Match a partial role or alias name into the internal role key. :param var: Game state :param role: Partial role to match on :param remove_spaces: Whether or not to remove all spaces before matching. This is meant for contexts where we truly cannot allow spaces somewhere; otherwise we should prefer that the user matches including spaces where possible for friendlier-looking commands. :param allow_extra: Whether to allow keys that are defined in the translation file but do not exist in the bot. Typically these are roles that were previously removed. :param allow_special: Whether to allow special keys (lover, vg activated, etc.). If scope is set, this parameter is ignored. :param scope: Limit matched roles to these explicitly passed-in roles (iterable of internal role names). :return: Match object with all matches (see src.match.match_all) """ role = role.lower() if remove_spaces: role = role.replace(" ", "") role_map = messages.get_role_mapping(reverse=True, remove_spaces=remove_spaces) special_keys = set() if scope is None and allow_special: evt = Event("get_role_metadata", {}) evt.dispatch(var, "special_keys") special_keys = functools.reduce(lambda x, y: x | y, evt.data.values(), special_keys) matches = match_all(role, role_map.keys()) # strip matches that don't refer to actual roles or special keys (i.e. refer to team names) filtered_matches = set() if scope is not None: allowed = set(scope) elif allow_extra: allowed = set(role_map.values()) | special_keys else: allowed = All.roles | special_keys for match in matches: if role_map[match] in allowed: filtered_matches.add(LocalRole(role_map[match], match)) return Match(filtered_matches)
def on_transition_night_end(evt, var): succubi = get_all_players(("succubus", )) role_map = messages.get_role_mapping() for succubus in succubi: pl = get_players() random.shuffle(pl) pl.remove(succubus) succ = [] for p in pl: if p in succubi: succ.append("{0} ({1})".format(p, role_map["succubus"])) else: succ.append(p.nick) succubus.send(messages["succubus_notify"], messages["players_list"].format(succ), sep="\n")
def complete_role(var, role: str, remove_spaces: bool = False, allow_special: bool = True) -> List[str]: """ Match a partial role or alias name into the internal role key. :param var: Game state :param role: Partial role to match on :param remove_spaces: Whether or not to remove all spaces before matching. This is meant for contexts where we truly cannot allow spaces somewhere; otherwise we should prefer that the user matches including spaces where possible for friendlier-looking commands. :param allow_special: Whether to allow special keys (lover, vg activated, etc.) :return: A list of 0 elements if the role didn't match anything. A list with 1 element containing the internal role key if the role matched unambiguously. A list with 2 or more elements containing localized role or alias names if the role had ambiguous matches. """ from src.cats import ROLES # FIXME: should this be moved into cats? ROLES isn't declared in cats.__all__ role = role.lower() if remove_spaces: role = role.replace(" ", "") role_map = messages.get_role_mapping(reverse=True, remove_spaces=remove_spaces) special_keys = set() if allow_special: evt = Event("get_role_metadata", {}) evt.dispatch(var, "special_keys") special_keys = functools.reduce(lambda x, y: x | y, evt.data.values(), special_keys) matches = complete_match(role, role_map.keys()) if not matches: return [] # strip matches that don't refer to actual roles or special keys (i.e. refer to team names) filtered_matches = [] allowed = ROLES.keys() | special_keys for match in matches: if role_map[match] in allowed: filtered_matches.append(match) if len(filtered_matches) == 1: return [role_map[filtered_matches[0]]] return filtered_matches
def parse_and_dispatch(var, wrapper: MessageDispatcher, key: str, message: str, role: Optional[str] = None, force: Optional[User] = None) -> None: """ Parses a command key and dispatches it should it match a valid command. :param var: Game state :param wrapper: Information about who is executing command and where command is being executed :param key: Command name. May be prefixed with command character and role name. :param message: Parameters to the command. :param role: Only dispatch a role command for the specified role. Even if a role name is specified in key, this will take precedence if set. :param force: Force the command to execute as the specified user instead of the current user. Admin and owner commands cannot be forced this way. When forcing a command, we set the appropriate context (channel vs PM) automatically as well. :return: """ _ignore_locals_ = True if key.startswith(botconfig.CMD_CHAR): key = key[len(botconfig.CMD_CHAR):] # check for role prefix parts = key.split(sep=":", maxsplit=1) if len(parts) > 1 and len(parts[0]): key = parts[1] role_prefix = parts[0] else: key = parts[0] role_prefix = None if role: role_prefix = role if not key: return if force: context = MessageDispatcher(force, wrapper.target) else: context = wrapper if role_prefix is not None: # match a role prefix to a role. Multi-word roles are supported by stripping the spaces matches = complete_role(var, role_prefix, remove_spaces=True) if len(matches) == 1: role_prefix = matches[0] elif len(matches) > 1: wrapper.pm(messages["ambiguous_role"].format(matches)) return else: wrapper.pm(messages["no_such_role"].format(role_prefix)) return # Don't change this into decorators.COMMANDS[key] even though it's a defaultdict, # as we don't want to insert bogus command keys into the dict. cmds = [] phase = var.PHASE if context.source in get_participants(): roles = get_all_roles(context.source) common_roles = set( roles) # roles shared by every eligible role command # A user can be a participant but not have a role, for example, dead vengeful ghost has_roles = len(roles) != 0 if role_prefix is not None: roles &= { role_prefix } # only fire off role commands for the user-specified role else: roles = set() common_roles = set() has_roles = False for fn in decorators.COMMANDS.get(key, []): if not fn.roles: cmds.append(fn) elif roles.intersection(fn.roles): cmds.append(fn) common_roles.intersection_update(fn.roles) if has_roles and not common_roles: # getting here means that at least one of the role_cmds is disjoint # from the others. For example, augur see vs seer see when a bare see # is executed. In this event, display a helpful error message instructing # the user to resolve the ambiguity. common_roles = set(roles) info = [0, 0] role_map = messages.get_role_mapping() for fn in cmds: fn_roles = roles.intersection(fn.roles) if not fn_roles: continue for role1 in common_roles: info[0] = role_map[role1].replace(" ", "") break for role2 in fn_roles: info[1] = role_map[role2].replace(" ", "") break common_roles &= fn_roles if not common_roles: break wrapper.pm(messages["ambiguous_command"].format(key, info[0], info[1])) return for fn in cmds: if force: if fn.owner_only or fn.flag: wrapper.pm(messages["no_force_admin"]) return if fn.chan: context.target = channels.Main else: context.target = users.Bot if phase == var.PHASE: # don't call any more commands if one we just called executed a phase transition fn.caller(var, context, message)