Ejemplo n.º 1
0
def end_who(cli, bot_server, bot_nick, target, rest):
    """Handle the end of WHO/WHOX responses from the server.

    Ordering and meaning of arguments for the end of a WHO/WHOX request:

    0 - The IRCClient instance (like everywhere else)
    1 - The server the requester (i.e. the bot) is on
    2 - The nickname of the requester (i.e. the bot)
    3 - The target the request was made against
    4 - A string containing some information; traditionally "End of /WHO list."

    This fires off the "who_end" event, and dispatches it with two
    arguments: The game state namespace and the channel or user the
    request was made to, or None if it could not be resolved.

    """

    try:
        target = channels.get(target)
    except KeyError:
        try:
            target = users._get(target)  # FIXME
        except KeyError:
            target = None
    else:
        if target._pending is not None:
            for name, params, args in target._pending:
                Event(name, params).dispatch(*args)
            target._pending = None

    Event("who_end", {}).dispatch(var, target)
Ejemplo n.º 2
0
def end_who(cli, bot_server, bot_nick, target, rest):
    """Handle the end of WHO/WHOX responses from the server.

    Ordering and meaning of arguments for the end of a WHO/WHOX request:

    0 - The IRCClient instance (like everywhere else)
    1 - The server the requester (i.e. the bot) is on
    2 - The nickname of the requester (i.e. the bot)
    3 - The target the request was made against
    4 - A string containing some information; traditionally "End of /WHO list."

    This fires off the "who_end" event, and dispatches it with one
    argument: The channel or user the request was made to, or None
    if it could not be resolved.

    """

    try:
        target = channels.get(target)
    except KeyError:
        try:
            target = users.get(target)
        except KeyError:
            target = None
    else:
        target.dispatch_queue()

    old = _who_old.get(target.name, target)
    _who_old.clear()
    Event("who_end", {}, old=old).dispatch(target)
Ejemplo n.º 3
0
def end_who(cli, bot_server, bot_nick, target, rest):
    """Handle the end of WHO/WHOX responses from the server.

    Ordering and meaning of arguments for the end of a WHO/WHOX request:

    0 - The IRCClient instance (like everywhere else)
    1 - The server the requester (i.e. the bot) is on
    2 - The nickname of the requester (i.e. the bot)
    3 - The target the request was made against
    4 - A string containing some information; traditionally "End of /WHO list."

    This fires off the "who_end" event, and dispatches it with two
    arguments: The game state namespace and the channel or user the
    request was made to, or None if it could not be resolved.

    """

    try:
        target = channels.get(target)
    except KeyError:
        try:
            target = users._get(target) # FIXME
        except KeyError:
            target = None
    else:
        if target._pending is not None:
            for name, params, args in target._pending:
                Event(name, params).dispatch(*args)
            target._pending = None

    Event("who_end", {}).dispatch(var, target)
Ejemplo n.º 4
0
def fwarn_del(var, wrapper, args):
    if args.help:
        wrapper.reply(messages["fwarn_del_syntax"])
        return

    warning = db.get_warning(args.id)
    if not warning:
        wrapper.reply(messages["fwarn_invalid_warning"])
        return

    warning["deleted_by"] = wrapper.source
    db.del_warning(args.id, wrapper.source.account)
    db.init_vars()
    wrapper.reply(messages["fwarn_done"])

    if var.LOG_CHANNEL:
        msg = messages["fwarn_log_del"].format(**warning)
        channels.get(var.LOG_CHANNEL).send(msg, prefix=var.LOG_PREFIX)
Ejemplo n.º 5
0
def who_reply(cli, bot_server, bot_nick, chan, ident, host, server, nick,
              status, hopcount_gecos):
    """Handle WHO replies for servers without WHOX support.

    Ordering and meaning of arguments for a bare WHO response:

    0 - The IRCClient instance (like everywhere else)
    1 - The server the requester (i.e. the bot) is on
    2 - The nickname of the requester (i.e. the bot)
    3 - The channel the request was made on
    4 - The ident of the user in this reply
    5 - The hostname of the user in this reply
    6 - The server the user in this reply is on
    7 - The nickname of the user in this reply
    8 - The status (H = Not away, G = Away, * = IRC operator, @ = Opped in the channel in 4, + = Voiced in the channel in 4)
    9 - The hop count and realname (gecos)

    This fires off the "who_result" event, and dispatches it with two
    arguments, a Channel and a User. Less important attributes can be
    accessed via the event.params namespace.

    """

    hop, realname = hopcount_gecos.split(" ", 1)
    # We throw away the information about the operness of the user, but we probably don't need to care about that
    # We also don't directly pass which modes they have, since that's already on the channel/user
    is_away = ("G" in status)

    modes = {Features["PREFIX"].get(s) for s in status} - {None}

    user = users.get(nick, ident, host, allow_bot=True, allow_none=True)
    if user is None:
        user = users.add(cli, nick=nick, ident=ident, host=host)

    ch = channels.get(chan, allow_none=True)
    if ch is not None and ch not in user.channels:
        user.channels[ch] = modes
        ch.users.add(user)
        for mode in modes:
            if mode not in ch.modes:
                ch.modes[mode] = set()
            ch.modes[mode].add(user)

    _who_old[user.nick] = user
    event = Event("who_result", {}, away=is_away, data=0, old=user)
    event.dispatch(ch, user)
Ejemplo n.º 6
0
def on_privmsg(cli, rawnick, chan, msg, *, notice=False):
    if notice and "!" not in rawnick or not rawnick:  # server notice; we don't care about those
        return

    _ignore_locals_ = False
    if var.USER_DATA_LEVEL == 0 or var.CHANNEL_DATA_LEVEL == 0:
        _ignore_locals_ = True  # don't expose in tb if we're trying to anonymize stuff

    user = users.get(rawnick, allow_none=True)

    ch = chan.lstrip("".join(Features["PREFIX"]))

    if users.equals(chan, users.Bot.nick):  # PM
        target = users.Bot
    else:
        target = channels.get(ch, allow_none=True)

    if user is None or target is None:
        return

    wrapper = MessageDispatcher(user, target)

    if wrapper.public and botconfig.IGNORE_HIDDEN_COMMANDS and not chan.startswith(
            tuple(Features["CHANTYPES"])):
        return

    if (notice and
        ((wrapper.public and not botconfig.ALLOW_NOTICE_COMMANDS) or
         (wrapper.private and not botconfig.ALLOW_PRIVATE_NOTICE_COMMANDS))):
        return  # not allowed in settings

    for fn in decorators.COMMANDS[""]:
        fn.caller(var, wrapper, msg)

    parts = msg.split(sep=" ", maxsplit=1)
    key = parts[0].lower()
    if len(parts) > 1:
        message = parts[1].strip()
    else:
        message = ""

    if wrapper.public and not key.startswith(botconfig.CMD_CHAR):
        return  # channel message but no prefix; ignore
    parse_and_dispatch(var, wrapper, key, message)
Ejemplo n.º 7
0
def on_whois_channels(cli, bot_server, bot_nick, nick, chans):
    """Handle WHOIS replies for the channels.

    Ordering and meaning of arguments for a WHOIS channels reply:

    0 - The IRCClient instance (like everywhere else)
    1 - The server the requester (i.e. the bot) is on
    2 - The nickname of the requester (i.e. the bot)
    3 - The nickname of the target
    4 - A space-separated string of channels

    This does not fire an event by itself, but sets up the proper data
    for the "endofwhois" listener to fire an event.

    """

    arg = "".join(Features["PREFIX"])
    for chan in chans.split(" "):
        ch = channels.get(chan.lstrip(arg), allow_none=True)
        if ch is not None:
            _whois_pending[nick]["channels"].add(ch)
Ejemplo n.º 8
0
def on_privmsg(cli, rawnick, chan, msg, *, notice=False, force_role=None):
    if notice and "!" not in rawnick or not rawnick: # server notice; we don't care about those
        return

    user = users._get(rawnick, allow_none=True) # FIXME

    if users.equals(chan, users.Bot.nick): # PM
        target = users.Bot
    else:
        target = channels.get(chan, allow_none=True)

    if user is None or target is None:
        return

    wrapper = MessageDispatcher(user, target)

    if wrapper.public and botconfig.IGNORE_HIDDEN_COMMANDS and not chan.startswith(tuple(hooks.Features["CHANTYPES"])):
        return

    if (notice and ((wrapper.public and not botconfig.ALLOW_NOTICE_COMMANDS) or
                    (wrapper.private and not botconfig.ALLOW_PRIVATE_NOTICE_COMMANDS))):
        return  # not allowed in settings

    if force_role is None: # if force_role isn't None, that indicates recursion; don't fire these off twice
        for fn in decorators.COMMANDS[""]:
            fn.caller(cli, rawnick, chan, msg)

    parts = msg.split(sep=" ", maxsplit=1)
    key = parts[0].lower()
    if len(parts) > 1:
        message = parts[1].lstrip()
    else:
        message = ""

    if wrapper.public and not key.startswith(botconfig.CMD_CHAR):
        return # channel message but no prefix; ignore

    if key.startswith(botconfig.CMD_CHAR):
        key = key[len(botconfig.CMD_CHAR):]

    if not key: # empty key ("") already handled above
        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 user in get_participants():
        roles = get_all_roles(user)
        # A user can be a participant but not have a role, for example, dead vengeful ghost
        has_roles = len(roles) != 0
        if force_role is not None:
            roles &= {force_role} # only fire off role commands for the forced role

        common_roles = set(roles) # roles shared by every eligible role command
        have_role_cmd = False
        for fn in decorators.COMMANDS.get(key, []):
            if not fn.roles:
                cmds.append(fn)
                continue
            if roles.intersection(fn.roles):
                have_role_cmd = True
                cmds.append(fn)
                common_roles.intersection_update(fn.roles)

        if force_role is not None and not have_role_cmd:
            # Trying to force a non-role command with a role.
            # We allow non-role commands to execute if a role is forced if a role
            # command is also executed, as this would allow (for example) a bot admin
            # to add extra effects to all "kill" commands without needing to continually
            # update the list of roles which can use "kill". However, we don't want to
            # allow things like "wolf pstats" because that just doesn't make sense.
            return

        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]
            for fn in cmds:
                fn_roles = roles.intersection(fn.roles)
                if not fn_roles:
                    continue
                for role1 in common_roles:
                    info[0] = role1
                    break
                for role2 in fn_roles:
                    info[1] = role2
                    break
                common_roles &= fn_roles
                if not common_roles:
                    break
            wrapper.pm(messages["ambiguous_command"].format(key, info[0], info[1]))
            return
    elif force_role is None:
        cmds = decorators.COMMANDS.get(key, [])

    for fn in cmds:
        if phase == var.PHASE:
            # FIXME: pass in var, wrapper, message instead of cli, rawnick, chan, message
            fn.caller(cli, rawnick, chan, message)
Ejemplo n.º 9
0
def extended_who_reply(cli, bot_server, bot_nick, data, chan, ident,
                       ip_address, host, server, nick, status, hop, idle,
                       account, realname):
    """Handle WHOX responses for servers that support it.

    An extended WHO (WHOX) is characterised by a second parameter to the request
    That parameter must be '%' followed by at least one of 'tcuihsnfdlar'
    If the 't' specifier is present, the specifiers must be followed by a comma and at most 3 bytes
    This is the ordering if all parameters are present, but not all of them are required
    If a parameter depends on a specifier, it will be stated at the front
    If a specifier is not given, the parameter will be omitted in the reply

    Ordering and meaning of arguments for an extended WHO (WHOX) response:

    0  -   - The IRCClient instance (like everywhere else)
    1  -   - The server the requester (i.e. the bot) is on
    2  -   - The nickname of the requester (i.e. the bot)
    3  - t - The data sent alongside the request
    4  - c - The channel the request was made on
    5  - u - The ident of the user in this reply
    6  - i - The IP address of the user in this reply
    7  - h - The hostname of the user in this reply
    8  - s - The server the user in this reply is on
    9  - n - The nickname of the user in this reply
    10 - f - Status (H = Not away, G = Away, * = IRC operator, @ = Opped in the channel in 5, + = Voiced in the channel in 5)
    11 - d - The hop count
    12 - l - The idle time (or 0 for users on other servers)
    13 - a - The services account name (or 0 if none/not logged in)
    14 - r - The realname (gecos)

    This fires off the "who_result" event, and dispatches it with two
    arguments, a Channel and a User. Less important attributes can be
    accessed via the event.params namespace.

    """

    if account == "0":
        account = None

    is_away = ("G" in status)

    data = int.from_bytes(data.encode(Features["CHARSET"]), "little")

    modes = {Features["PREFIX"].get(s) for s in status} - {None}

    # WHOX may be issued to retrieve updated account info so exclude account from users.get()
    # we handle the account change differently below and don't want to add duplicate users
    user = users.get(nick, ident, host, allow_bot=True, allow_none=True)
    if user is None:
        user = users.add(cli,
                         nick=nick,
                         ident=ident,
                         host=host,
                         account=account)

    new_user = user
    if {user.account, account} != {None} and not context.equals(
            user.account, account):
        # first check tests if both are None, and skips over this if so
        old_account = user.account
        user.account = account
        new_user = users.get(nick, ident, host, account, allow_bot=True)
        Event("account_change", {}, old=user).dispatch(new_user, old_account)

    ch = channels.get(chan, allow_none=True)
    if ch is not None and ch not in user.channels:
        user.channels[ch] = modes
        ch.users.add(user)
        for mode in modes:
            if mode not in ch.modes:
                ch.modes[mode] = set()
            ch.modes[mode].add(user)

    _who_old[new_user.nick] = user
    event = Event("who_result", {}, away=is_away, data=data, old=user)
    event.dispatch(ch, new_user)
Ejemplo n.º 10
0
    def caller(self, cli, rawnick, chan, rest):
        _ignore_locals_ = True
        user = users._get(rawnick, allow_none=True) # FIXME

        if users.equals(chan, users.Bot.nick): # PM
            target = users.Bot
        else:
            target = channels.get(chan, allow_none=True)

        if user is None or target is None:
            return

        dispatcher = MessageDispatcher(user, target)

        if (not self.pm and dispatcher.private) or (not self.chan and dispatcher.public):
            return # channel or PM command that we don't allow

        if dispatcher.public and target is not channels.Main and not (self.flag or self.owner_only):
            if "" in self.commands or not self.alt_allowed:
                return # commands not allowed in alt channels

        if "" in self.commands:
            self.func(var, dispatcher, rest)
            return

        if self.phases and var.PHASE not in self.phases:
            return

        if self.playing and (user not in get_players() or user in var.DISCONNECTED):
            return

        for role in self.roles:
            if user in var.ROLES[role]:
                break
        else:
            if (self.users is not None and user not in self.users) or self.roles:
                return

        if self.silenced and user.nick in var.SILENCED: # FIXME: Need to change this once var.SILENCED holds User instances
            dispatcher.pm(messages["silenced"])
            return

        if self.roles or (self.users is not None and user in self.users):
            self.func(var, dispatcher, rest) # don't check restrictions for role commands
            # Role commands might end the night if it's nighttime
            if var.PHASE == "night":
                from src.wolfgame import chk_nightdone
                chk_nightdone()
            return

        if self.owner_only:
            if user.is_owner():
                adminlog(chan, rawnick, self.name, rest)
                self.func(var, dispatcher, rest)
                return

            dispatcher.pm(messages["not_owner"])
            return

        temp = user.lower()

        flags = var.FLAGS[temp.rawnick] + var.FLAGS_ACCS[temp.account] # TODO: add flags handling to User

        if self.flag and (user.is_admin() or user.is_owner()):
            adminlog(chan, rawnick, self.name, rest)
            return self.func(var, dispatcher, rest)

        denied_commands = var.DENY[temp.rawnick] | var.DENY_ACCS[temp.account] # TODO: add denied commands handling to User

        if self.commands & denied_commands:
            dispatcher.pm(messages["invalid_permissions"])
            return

        if self.flag:
            if self.flag in flags:
                adminlog(chan, rawnick, self.name, rest)
                self.func(var, dispatcher, rest)
                return

            dispatcher.pm(messages["not_an_admin"])
            return

        self.func(var, dispatcher, rest)
Ejemplo n.º 11
0
def fwarn(var, wrapper, message):
    """Issues a warning to someone or views warnings."""
    # !fwarn list [-all] [nick] [page]
    # -all => Shows all warnings, if omitted only shows active (non-expired and non-deleted) ones.
    # nick => nick to view warnings for. Can also be a hostmask in nick!user@host form. If nick
    #     is not online, interpreted as an account name. To specify an account if nick is online,
    #     use =account. If not specified, shows all warnings on the bot.
    # !fwarn view <id> - views details on warning id
    # !fwarn del <id> - deletes warning id
    # !fwarn set <id> [~expiry] [reason] [| notes]
    # !fwarn add <nick> <points> [~expiry] [sanctions] [:]<reason> [| notes]
    # e.g. !fwarn add lykos 1 ~30d deny=goat,gstats stasis=5 Spamming | I secretly just hate him
    # nick => nick to warn. Can also be a hostmask in nick!user@host form. If nick is not online,
    #    interpreted as an account name. To specify an account if nick is online, use =account.
    # points => Warning points, must be above 0
    # ~expiry => Expiration time, must be suffixed with d (days), h (hours), or m (minutes)
    # sanctions => list of sanctions. Valid sanctions are:
    #    deny: denies access to the listed commands
    #    stasis: gives the user stasis
    # reason => Reason, required. If the first word of the reason is also a sanction, prefix it with :
    # |notes => Secret notes, not shown to the user (only shown if viewing the warning in PM)
    #    If specified, must be prefixed with |. This means | is not a valid character for use
    #    in reasons (no escaping is performed).

    params = message.split()
    target = None
    points = None
    expires = None
    sanctions = {}
    reason = None
    notes = None

    try:
        command = params.pop(0)
    except IndexError:
        wrapper.reply(messages["fwarn_usage"])
        return

    if command not in ("list", "view", "add", "del", "set", "help"):
        # if what follows is a number, assume we're viewing or setting a warning
        # (depending on number of params)
        # if it's another string, assume we're adding or listing, again depending
        # on number of params
        params.insert(0, command)
        try:
            num = int(command)
            if len(params) == 1:
                command = "view"
            else:
                command = "set"
        except ValueError:
            if len(params) < 3 or params[1] == "-all":
                command = "list"
                if len(params) > 1 and params[1] == "-all":
                    # fwarn list expects these two params in a different order
                    params.pop(1)
                    params.insert(0, "-all")
            else:
                command = "add"

    if command == "help":
        try:
            subcommand = params.pop(0)
        except IndexError:
            wrapper.reply(messages["fwarn_usage"])
            return
        if subcommand not in ("list", "view", "add", "del", "set", "help"):
            wrapper.reply(messages["fwarn_usage"])
            return
        wrapper.reply(messages["fwarn_{0}_syntax".format(subcommand)])
        return

    if command == "list":
        list_all = False
        page = 1
        try:
            list_all = params.pop(0)
            target = params.pop(0)
            page = int(params.pop(0))
        except IndexError:
            pass
        except ValueError:
            wrapper.reply(messages["fwarn_page_invalid"])
            return

        try:
            if list_all and list_all != "-all":
                if target is not None:
                    page = int(target)
                target = list_all
                list_all = False
            elif list_all == "-all":
                list_all = True
        except ValueError:
            wrapper.reply(messages["fwarn_page_invalid"])
            return

        try:
            page = int(target)
            target = None
        except (TypeError, ValueError):
            pass

        if target is not None:
            acc, hm = parse_warning_target(target)
            if acc is None and hm is None:
                wrapper.reply(messages["fwarn_nick_invalid"])
                return
            warnings = db.list_warnings(acc,
                                        hm,
                                        expired=list_all,
                                        deleted=list_all,
                                        skip=(page - 1) * 10,
                                        show=11)
            points = db.get_warning_points(acc, hm)
            wrapper.pm(messages["fwarn_list_header"].format(
                target, points, "" if points == 1 else "s"))
        else:
            warnings = db.list_all_warnings(list_all=list_all,
                                            skip=(page - 1) * 10,
                                            show=11)

        i = 0
        for warn in warnings:
            i += 1
            if (i == 11):
                parts = []
                if list_all:
                    parts.append("-all")
                if target is not None:
                    parts.append(target)
                parts.append(str(page + 1))
                wrapper.pm(messages["fwarn_list_footer"].format(
                    " ".join(parts)))
                break
            start = ""
            end = ""
            ack = ""
            if warn["expires"] is not None:
                if warn["expired"]:
                    expires = messages["fwarn_list_expired"].format(
                        warn["expires"])
                else:
                    expires = messages["fwarn_view_expires"].format(
                        warn["expires"])
            else:
                expires = messages["fwarn_never_expires"]
            if warn["deleted"]:
                start = "\u000314"
                end = " [\u00034{0}\u000314]\u0003".format(
                    messages["fwarn_deleted"])
            elif warn["expired"]:
                start = "\u000314"
                end = " [\u00037{0}\u000314]\u0003".format(
                    messages["fwarn_expired"])
            if not warn["ack"]:
                ack = "\u0002!\u0002 "
            wrapper.pm(messages["fwarn_list"].format(
                start, ack, warn["id"], warn["issued"], warn["target"],
                warn["sender"], warn["reason"], warn["amount"],
                "" if warn["amount"] == 1 else "s", expires, end))
        if i == 0:
            wrapper.pm(messages["fwarn_list_empty"])
        return

    if command == "view":
        try:
            warn_id = params.pop(0)
            if warn_id[0] == "#":
                warn_id = warn_id[1:]
            warn_id = int(warn_id)
        except (IndexError, ValueError):
            wrapper.reply(messages["fwarn_view_syntax"])
            return

        warning = db.get_warning(warn_id)
        if warning is None:
            wrapper.reply(messages["fwarn_invalid_warning"])
            return

        if warning["deleted"]:
            expires = messages["fwarn_view_deleted"].format(
                warning["deleted_on"], warning["deleted_by"])
        elif warning["expired"]:
            expires = messages["fwarn_view_expired"].format(warning["expires"])
        elif warning["expires"] is None:
            expires = messages["fwarn_view_active"].format(
                messages["fwarn_never_expires"])
        else:
            expires = messages["fwarn_view_active"].format(
                messages["fwarn_view_expires"].format(warning["expires"]))

        wrapper.pm(messages["fwarn_view_header"].format(
            warning["id"], warning["target"], warning["issued"],
            warning["sender"], warning["amount"],
            "" if warning["amount"] == 1 else "s", expires))

        reason = [warning["reason"]]
        if warning["notes"] is not None:
            reason.append(warning["notes"])
        wrapper.pm(" | ".join(reason))

        sanctions = []
        if not warning["ack"]:
            sanctions.append(messages["fwarn_view_ack"])
        if warning["sanctions"]:
            sanctions.append(messages["fwarn_view_sanctions"])
            if "stasis" in warning["sanctions"]:
                if warning["sanctions"]["stasis"] != 1:
                    sanctions.append(
                        messages["fwarn_view_stasis_plural"].format(
                            warning["sanctions"]["stasis"]))
                else:
                    sanctions.append(messages["fwarn_view_stasis_sing"])
            if "deny" in warning["sanctions"]:
                sanctions.append(messages["fwarn_view_deny"].format(", ".join(
                    warning["sanctions"]["deny"])))
            if "tempban" in warning["sanctions"]:
                sanctions.append(messages["fwarn_view_tempban"].format(
                    warning["sanctions"]["tempban"]))
        if sanctions:
            wrapper.pm(" ".join(sanctions))
        return

    if command == "del":
        try:
            warn_id = params.pop(0)
            if warn_id[0] == "#":
                warn_id = warn_id[1:]
            warn_id = int(warn_id)
        except (IndexError, ValueError):
            wrapper.reply(messages["fwarn_del_syntax"])
            return

        warning = db.get_warning(warn_id)
        if warning is None:
            wrapper.reply(messages["fwarn_invalid_warning"])
            return

        acc, hm = parse_warning_target(wrapper.source.nick)
        db.del_warning(warn_id, acc, hm)
        wrapper.reply(messages["fwarn_done"])

        if var.LOG_CHANNEL:
            msg = messages["fwarn_log_del"].format(
                warn_id, warning["target"], hm, warning["reason"],
                (" | " + warning["notes"]) if warning["notes"] else "")
            channels.get(var.LOG_CHANNEL).send(msg, prefix=var.LOG_PREFIX)
        return

    if command == "set":
        try:
            warn_id = params.pop(0)
            if warn_id[0] == "#":
                warn_id = warn_id[1:]
            warn_id = int(warn_id)
        except (IndexError, ValueError):
            wrapper.reply(messages["fwarn_set_syntax"])
            return

        warning = db.get_warning(warn_id)
        if warning is None:
            wrapper.reply(messages["fwarn_invalid_warning"])
            return

        rsp = " ".join(params).split("|", 1)
        if len(rsp) == 1:
            rsp.append(None)
        reason, notes = rsp
        reason = reason.strip()

        # check for modified expiry
        expires = warning["expires"]
        rsp = reason.split(" ", 1)
        if rsp[0] and rsp[0][0] == "~":
            if len(rsp) == 1:
                rsp.append("")
            expires, reason = rsp
            expires = expires[1:]
            reason = reason.strip()

            if expires in messages["never_aliases"]:
                expires = None
            else:
                suffix = expires[-1]
                try:
                    amount = int(expires[:-1])
                except ValueError:
                    wrapper.reply(messages["fwarn_expiry_invalid"])
                    return

                if amount <= 0:
                    wrapper.reply(messages["fwarn_expiry_invalid"])
                    return

                issued = datetime.strptime(warning["issued"],
                                           "%Y-%m-%d %H:%M:%S")
                if suffix == "d":
                    expires = issued + timedelta(days=amount)
                elif suffix == "h":
                    expires = issued + timedelta(hours=amount)
                elif suffix == "m":
                    expires = issued + timedelta(minutes=amount)
                else:
                    wrapper.reply(messages["fwarn_expiry_invalid"])
                    return

                round_add = 0
                if expires.second >= 30:
                    round_add = 1
                expires -= timedelta(seconds=expires.second,
                                     microseconds=expires.microsecond)
                expires += timedelta(minutes=round_add)

        # maintain existing reason if none was specified
        if not reason:
            reason = warning["reason"]

        # maintain existing notes if none were specified
        if notes is not None:
            notes = notes.strip()
            if not notes:
                notes = None
        else:
            notes = warning["notes"]

        db.set_warning(warn_id, expires, reason, notes)
        wrapper.reply(messages["fwarn_done"])

        if var.LOG_CHANNEL:
            changes = []
            if expires != warning["expires"]:
                oldexpiry = warning["expires"] if warning[
                    "expires"] else messages["fwarn_log_set_noexpiry"]
                newexpiry = expires if expires else messages[
                    "fwarn_log_set_noexpiry"]
                changes.append(messages["fwarn_log_set_expiry"].format(
                    oldexpiry, newexpiry))
            if reason != warning["reason"]:
                changes.append(messages["fwarn_log_set_reason"].format(
                    warning["reason"], reason))
            if notes != warning["notes"]:
                if warning["notes"]:
                    changes.append(messages["fwarn_log_set_notes"].format(
                        warning["notes"], notes))
                else:
                    changes.append(
                        messages["fwarn_log_set_notes_new"].format(notes))
            if changes:
                log_msg = messages["fwarn_log_set"].format(
                    warn_id, warning["target"], wrapper.source.nick,
                    " | ".join(changes))
                channels.get(var.LOG_CHANNEL).send(log_msg,
                                                   prefix=var.LOG_PREFIX)

        return

    # command == "add"
    while params:
        p = params.pop(0)
        if target is None:
            # figuring out what target actually is is handled in add_warning
            target = p
        elif points is None:
            try:
                points = int(p)
            except ValueError:
                wrapper.reply(messages["fwarn_points_invalid"])
                return
            if points < 0:
                wrapper.reply(messages["fwarn_points_invalid"])
                return
        elif notes is not None:
            notes += " " + p
        elif reason is not None:
            rsp = p.split("|", 1)
            if len(rsp) > 1:
                notes = rsp[1]
            reason += " " + rsp[0]
        elif p[0] == ":":
            if p == ":":
                reason = ""
            else:
                reason = p[1:]
        elif p[0] == "~":
            if p == "~":
                wrapper.reply(messages["fwarn_syntax"])
                return
            expires = p[1:]
        else:
            # sanctions are the only thing left here
            sanc = p.split("=", 1)
            if sanc[0] == "deny":
                try:
                    cmds = sanc[1].split(",")
                    normalized_cmds = set()
                    for cmd in cmds:
                        normalized = None
                        for obj in COMMANDS[cmd]:
                            # do not allow denying in-game commands (vote, see, etc.)
                            # this technically traps goat too, so special case that, as we want
                            # goat to be deny-able. Furthermore, the warn command cannot be denied.
                            if (not obj.playing
                                    and not obj.roles) or obj.name == "goat":
                                normalized = obj.name
                            if normalized == "warn":
                                normalized = None
                        if normalized is None:
                            wrapper.reply(
                                messages["fwarn_deny_invalid_command"].format(
                                    cmd))
                            return
                        normalized_cmds.add(normalized)
                    sanctions["deny"] = normalized_cmds
                except IndexError:
                    wrapper.reply(messages["fwarn_deny_invalid"])
                    return
            elif sanc[0] == "stasis":
                try:
                    sanctions["stasis"] = int(sanc[1])
                except (IndexError, ValueError):
                    wrapper.reply(messages["fwarn_stasis_invalid"])
                    return
            elif sanc[0] == "tempban":
                try:
                    banamt = sanc[1]
                    suffix = banamt[-1]
                    if suffix not in ("d", "h", "m"):
                        sanctions["tempban"] = int(banamt)
                    else:
                        banamt = int(banamt[:-1])
                        if suffix == "d":
                            sanctions["tempban"] = datetime.utcnow(
                            ) + timedelta(days=banamt)
                        elif suffix == "h":
                            sanctions["tempban"] = datetime.utcnow(
                            ) + timedelta(hours=banamt)
                        elif suffix == "m":
                            sanctions["tempban"] = datetime.utcnow(
                            ) + timedelta(minutes=banamt)
                except (IndexError, ValueError):
                    wrapper.reply(messages["fwarn_tempban_invalid"])
                    return
            else:
                # not a valid sanction, assume this is the start of the reason
                reason = p

    if target is None or points is None or reason is None:
        wrapper.reply(messages["fwarn_add_syntax"])
        return

    reason = reason.strip()
    if notes is not None:
        notes = notes.strip()

    # convert expires into a proper datetime
    if expires is None:
        expires = var.DEFAULT_EXPIRY

    if expires.lower() in messages["never_aliases"]:
        expires = None

    try:
        warn_id = add_warning(wrapper.client, target, points,
                              wrapper.source.nick, reason, notes, expires,
                              sanctions)  # FIXME
    except ValueError:
        wrapper.reply(messages["fwarn_expiry_invalid"])
        return

    if warn_id is False:
        wrapper.reply(messages["fwarn_cannot_add"])
    else:
        wrapper.reply(messages["fwarn_added"].format(warn_id))
        # Log to ops/log channel (even if the warning was placed in that channel)
        if var.LOG_CHANNEL:
            log_reason = reason
            if notes is not None:
                log_reason += " | " + notes
            if expires is None:
                log_length = messages["fwarn_log_add_noexpiry"]
            else:
                log_length = messages["fwarn_log_add_expiry"].format(expires)
            log_msg = messages["fwarn_log_add"].format(
                warn_id, target, wrapper.source.nick, log_reason, points,
                "" if points == 1 else "s", log_length)
            channels.get(var.LOG_CHANNEL).send(log_msg, prefix=var.LOG_PREFIX)
Ejemplo n.º 12
0
def fwarn(var, wrapper, message):
    """Issues a warning to someone or views warnings."""
    # !fwarn list [-all] [nick] [page]
    # -all => Shows all warnings, if omitted only shows active (non-expired and non-deleted) ones.
    # nick => nick to view warnings for. Can also be a hostmask in nick!user@host form. If nick
    #     is not online, interpreted as an account name. To specify an account if nick is online,
    #     use =account. If not specified, shows all warnings on the bot.
    # !fwarn view <id> - views details on warning id
    # !fwarn del <id> - deletes warning id
    # !fwarn set <id> [~expiry] [reason] [| notes]
    # !fwarn add <nick> <points> [~expiry] [sanctions] [:]<reason> [| notes]
    # e.g. !fwarn add lykos 1 ~30d deny=goat,gstats stasis=5 Spamming | I secretly just hate him
    # nick => nick to warn. Can also be a hostmask in nick!user@host form. If nick is not online,
    #    interpreted as an account name. To specify an account if nick is online, use =account.
    # points => Warning points, must be above 0
    # ~expiry => Expiration time, must be suffixed with d (days), h (hours), or m (minutes)
    # sanctions => list of sanctions. Valid sanctions are:
    #    deny: denies access to the listed commands
    #    stasis: gives the user stasis
    # reason => Reason, required. If the first word of the reason is also a sanction, prefix it with :
    # |notes => Secret notes, not shown to the user (only shown if viewing the warning in PM)
    #    If specified, must be prefixed with |. This means | is not a valid character for use
    #    in reasons (no escaping is performed).

    params = message.split()
    target = None
    points = None
    expires = None
    sanctions = {}
    reason = None
    notes = None

    try:
        command = params.pop(0)
    except IndexError:
        wrapper.reply(messages["fwarn_usage"])
        return

    if command not in ("list", "view", "add", "del", "set", "help"):
        # if what follows is a number, assume we're viewing or setting a warning
        # (depending on number of params)
        # if it's another string, assume we're adding or listing, again depending
        # on number of params
        params.insert(0, command)
        try:
            num = int(command)
            if len(params) == 1:
                command = "view"
            else:
                command = "set"
        except ValueError:
            if len(params) < 3 or params[1] == "-all":
                command = "list"
                if len(params) > 1 and params[1] == "-all":
                    # fwarn list expects these two params in a different order
                    params.pop(1)
                    params.insert(0, "-all")
            else:
                command = "add"

    if command == "help":
        try:
            subcommand = params.pop(0)
        except IndexError:
            wrapper.reply(messages["fwarn_usage"])
            return
        if subcommand not in ("list", "view", "add", "del", "set", "help"):
            wrapper.reply(messages["fwarn_usage"])
            return
        wrapper.reply(messages["fwarn_{0}_syntax".format(subcommand)])
        return

    if command == "list":
        list_all = False
        page = 1
        try:
            list_all = params.pop(0)
            target = params.pop(0)
            page = int(params.pop(0))
        except IndexError:
            pass
        except ValueError:
            wrapper.reply(messages["fwarn_page_invalid"])
            return

        try:
            if list_all and list_all != "-all":
                if target is not None:
                    page = int(target)
                target = list_all
                list_all = False
            elif list_all == "-all":
                list_all = True
        except ValueError:
            wrapper.reply(messages["fwarn_page_invalid"])
            return

        try:
            page = int(target)
            target = None
        except (TypeError, ValueError):
            pass

        if target is not None:
            acc, hm = parse_warning_target(target)
            if acc is None and hm is None:
                wrapper.reply(messages["fwarn_nick_invalid"])
                return
            warnings = db.list_warnings(acc, hm, expired=list_all, deleted=list_all, skip=(page-1)*10, show=11)
            points = db.get_warning_points(acc, hm)
            wrapper.pm(messages["fwarn_list_header"].format(target, points, "" if points == 1 else "s"))
        else:
            warnings = db.list_all_warnings(list_all=list_all, skip=(page-1)*10, show=11)

        i = 0
        for warn in warnings:
            i += 1
            if (i == 11):
                parts = []
                if list_all:
                    parts.append("-all")
                if target is not None:
                    parts.append(target)
                parts.append(str(page + 1))
                wrapper.pm(messages["fwarn_list_footer"].format(" ".join(parts)))
                break
            start = ""
            end = ""
            ack = ""
            if warn["expires"] is not None:
                if warn["expired"]:
                    expires = messages["fwarn_list_expired"].format(warn["expires"])
                else:
                    expires = messages["fwarn_view_expires"].format(warn["expires"])
            else:
                expires = messages["fwarn_never_expires"]
            if warn["deleted"]:
                start = "\u000314"
                end = " [\u00034{0}\u000314]\u0003".format(messages["fwarn_deleted"])
            elif warn["expired"]:
                start = "\u000314"
                end = " [\u00037{0}\u000314]\u0003".format(messages["fwarn_expired"])
            if not warn["ack"]:
                ack = "\u0002!\u0002 "
            wrapper.pm(messages["fwarn_list"].format(
                start, ack, warn["id"], warn["issued"], warn["target"],
                warn["sender"], warn["reason"], warn["amount"],
                "" if warn["amount"] == 1 else "s", expires, end))
        if i == 0:
            wrapper.pm(messages["fwarn_list_empty"])
        return

    if command == "view":
        try:
            warn_id = params.pop(0)
            if warn_id[0] == "#":
                warn_id = warn_id[1:]
            warn_id = int(warn_id)
        except (IndexError, ValueError):
            wrapper.reply(messages["fwarn_view_syntax"])
            return

        warning = db.get_warning(warn_id)
        if warning is None:
            wrapper.reply(messages["fwarn_invalid_warning"])
            return

        if warning["deleted"]:
            expires = messages["fwarn_view_deleted"].format(warning["deleted_on"], warning["deleted_by"])
        elif warning["expired"]:
            expires = messages["fwarn_view_expired"].format(warning["expires"])
        elif warning["expires"] is None:
            expires = messages["fwarn_view_active"].format(messages["fwarn_never_expires"])
        else:
            expires = messages["fwarn_view_active"].format(messages["fwarn_view_expires"].format(warning["expires"]))

        wrapper.pm(messages["fwarn_view_header"].format(
            warning["id"], warning["target"], warning["issued"], warning["sender"],
            warning["amount"], "" if warning["amount"] == 1 else "s", expires))

        reason = [warning["reason"]]
        if warning["notes"] is not None:
            reason.append(warning["notes"])
        wrapper.pm(" | ".join(reason))

        sanctions = []
        if not warning["ack"]:
            sanctions.append(messages["fwarn_view_ack"])
        if warning["sanctions"]:
            sanctions.append(messages["fwarn_view_sanctions"])
            if "stasis" in warning["sanctions"]:
                if warning["sanctions"]["stasis"] != 1:
                    sanctions.append(messages["fwarn_view_stasis_plural"].format(warning["sanctions"]["stasis"]))
                else:
                    sanctions.append(messages["fwarn_view_stasis_sing"])
            if "deny" in warning["sanctions"]:
                sanctions.append(messages["fwarn_view_deny"].format(", ".join(warning["sanctions"]["deny"])))
            if "tempban" in warning["sanctions"]:
                sanctions.append(messages["fwarn_view_tempban"].format(warning["sanctions"]["tempban"]))
        if sanctions:
            wrapper.pm(" ".join(sanctions))
        return

    if command == "del":
        try:
            warn_id = params.pop(0)
            if warn_id[0] == "#":
                warn_id = warn_id[1:]
            warn_id = int(warn_id)
        except (IndexError, ValueError):
            wrapper.reply(messages["fwarn_del_syntax"])
            return

        warning = db.get_warning(warn_id)
        if warning is None:
            wrapper.reply(messages["fwarn_invalid_warning"])
            return

        acc, hm = parse_warning_target(wrapper.source.nick)
        db.del_warning(warn_id, acc, hm)
        wrapper.reply(messages["fwarn_done"])

        if var.LOG_CHANNEL:
            msg = messages["fwarn_log_del"].format(
                warn_id, warning["target"], hm,
                warning["reason"], (" | " + warning["notes"]) if warning["notes"] else "")
            channels.get(var.LOG_CHANNEL).send(msg, prefix=var.LOG_PREFIX)
        return

    if command == "set":
        try:
            warn_id = params.pop(0)
            if warn_id[0] == "#":
                warn_id = warn_id[1:]
            warn_id = int(warn_id)
        except (IndexError, ValueError):
            wrapper.reply(messages["fwarn_set_syntax"])
            return

        warning = db.get_warning(warn_id)
        if warning is None:
            wrapper.reply(messages["fwarn_invalid_warning"])
            return

        rsp = " ".join(params).split("|", 1)
        if len(rsp) == 1:
            rsp.append(None)
        reason, notes = rsp
        reason = reason.strip()

        # check for modified expiry
        expires = warning["expires"]
        rsp = reason.split(" ", 1)
        if rsp[0] and rsp[0][0] == "~":
            if len(rsp) == 1:
                rsp.append("")
            expires, reason = rsp
            expires = expires[1:]
            reason = reason.strip()

            if expires in messages["never_aliases"]:
                expires = None
            else:
                suffix = expires[-1]
                try:
                    amount = int(expires[:-1])
                except ValueError:
                    wrapper.reply(messages["fwarn_expiry_invalid"])
                    return

                if amount <= 0:
                    wrapper.reply(messages["fwarn_expiry_invalid"])
                    return

                issued = datetime.strptime(warning["issued"], "%Y-%m-%d %H:%M:%S")
                if suffix == "d":
                    expires = issued + timedelta(days=amount)
                elif suffix == "h":
                    expires = issued + timedelta(hours=amount)
                elif suffix == "m":
                    expires = issued + timedelta(minutes=amount)
                else:
                    wrapper.reply(messages["fwarn_expiry_invalid"])
                    return

                round_add = 0
                if expires.second >= 30:
                    round_add = 1
                expires -= timedelta(seconds=expires.second, microseconds=expires.microsecond)
                expires += timedelta(minutes=round_add)

        # maintain existing reason if none was specified
        if not reason:
            reason = warning["reason"]

        # maintain existing notes if none were specified
        if notes is not None:
            notes = notes.strip()
            if not notes:
                notes = None
        else:
            notes = warning["notes"]

        db.set_warning(warn_id, expires, reason, notes)
        wrapper.reply(messages["fwarn_done"])

        if var.LOG_CHANNEL:
            changes = []
            if expires != warning["expires"]:
                oldexpiry = warning["expires"] if warning["expires"] else messages["fwarn_log_set_noexpiry"]
                newexpiry = expires if expires else messages["fwarn_log_set_noexpiry"]
                changes.append(messages["fwarn_log_set_expiry"].format(oldexpiry, newexpiry))
            if reason != warning["reason"]:
                changes.append(messages["fwarn_log_set_reason"].format(warning["reason"], reason))
            if notes != warning["notes"]:
                if warning["notes"]:
                    changes.append(messages["fwarn_log_set_notes"].format(warning["notes"], notes))
                else:
                    changes.append(messages["fwarn_log_set_notes_new"].format(notes))
            if changes:
                log_msg = messages["fwarn_log_set"].format(warn_id, warning["target"], wrapper.source.nick, " | ".join(changes))
                channels.get(var.LOG_CHANNEL).send(log_msg, prefix=var.LOG_PREFIX)

        return

    # command == "add"
    while params:
        p = params.pop(0)
        if target is None:
            # figuring out what target actually is is handled in add_warning
            target = p
        elif points is None:
            try:
                points = int(p)
            except ValueError:
                wrapper.reply(messages["fwarn_points_invalid"])
                return
            if points < 0:
                wrapper.reply(messages["fwarn_points_invalid"])
                return
        elif notes is not None:
            notes += " " + p
        elif reason is not None:
            rsp = p.split("|", 1)
            if len(rsp) > 1:
                notes = rsp[1]
            reason += " " + rsp[0]
        elif p[0] == ":":
            if p == ":":
                reason = ""
            else:
                reason = p[1:]
        elif p[0] == "~":
            if p == "~":
                wrapper.reply(messages["fwarn_syntax"])
                return
            expires = p[1:]
        else:
            # sanctions are the only thing left here
            sanc = p.split("=", 1)
            if sanc[0] == "deny":
                try:
                    cmds = sanc[1].split(",")
                    normalized_cmds = set()
                    for cmd in cmds:
                        normalized = None
                        for obj in COMMANDS[cmd]:
                            # do not allow denying in-game commands (vote, see, etc.)
                            # this technically traps goat too, so special case that, as we want
                            # goat to be deny-able. Furthermore, the warn command cannot be denied.
                            if (not obj.playing and not obj.roles) or obj.name == "goat":
                                normalized = obj.name
                            if normalized == "warn":
                                normalized = None
                        if normalized is None:
                            wrapper.reply(messages["fwarn_deny_invalid_command"].format(cmd))
                            return
                        normalized_cmds.add(normalized)
                    sanctions["deny"] = normalized_cmds
                except IndexError:
                    wrapper.reply(messages["fwarn_deny_invalid"])
                    return
            elif sanc[0] == "stasis":
                try:
                    sanctions["stasis"] = int(sanc[1])
                except (IndexError, ValueError):
                    wrapper.reply(messages["fwarn_stasis_invalid"])
                    return
            elif sanc[0] == "tempban":
                try:
                    banamt = sanc[1]
                    suffix = banamt[-1]
                    if suffix not in ("d", "h", "m"):
                        sanctions["tempban"] = int(banamt)
                    else:
                        banamt = int(banamt[:-1])
                        if suffix == "d":
                            sanctions["tempban"] = datetime.utcnow() + timedelta(days=banamt)
                        elif suffix == "h":
                            sanctions["tempban"] = datetime.utcnow() + timedelta(hours=banamt)
                        elif suffix == "m":
                            sanctions["tempban"] = datetime.utcnow() + timedelta(minutes=banamt)
                except (IndexError, ValueError):
                    wrapper.reply(messages["fwarn_tempban_invalid"])
                    return
            else:
                # not a valid sanction, assume this is the start of the reason
                reason = p

    if target is None or points is None or reason is None:
        wrapper.reply(messages["fwarn_add_syntax"])
        return

    reason = reason.strip()
    if notes is not None:
        notes = notes.strip()

    # convert expires into a proper datetime
    if expires is None:
        expires = var.DEFAULT_EXPIRY

    if expires.lower() in messages["never_aliases"]:
        expires = None

    try:
        warn_id = add_warning(wrapper.client, target, points, wrapper.source.nick, reason, notes, expires, sanctions) # FIXME
    except ValueError:
        wrapper.reply(messages["fwarn_expiry_invalid"])
        return

    if warn_id is False:
        wrapper.reply(messages["fwarn_cannot_add"])
    else:
        wrapper.reply(messages["fwarn_added"].format(warn_id))
        # Log to ops/log channel (even if the warning was placed in that channel)
        if var.LOG_CHANNEL:
            log_reason = reason
            if notes is not None:
                log_reason += " | " + notes
            if expires is None:
                log_length = messages["fwarn_log_add_noexpiry"]
            else:
                log_length = messages["fwarn_log_add_expiry"].format(expires)
            log_msg = messages["fwarn_log_add"].format(warn_id, target, wrapper.source.nick, log_reason, points,
                                                       "" if points == 1 else "s", log_length)
            channels.get(var.LOG_CHANNEL).send(log_msg, prefix=var.LOG_PREFIX)
Ejemplo n.º 13
0
def on_privmsg(cli, rawnick, chan, msg, *, notice=False, force_role=None):
    if notice and "!" not in rawnick or not rawnick: # server notice; we don't care about those
        return

    user = users._get(rawnick, allow_none=True) # FIXME

    ch = chan.lstrip("".join(hooks.Features["PREFIX"]))

    if users.equals(chan, users.Bot.nick): # PM
        target = users.Bot
    else:
        target = channels.get(ch, allow_none=True)

    if user is None or target is None:
        return

    wrapper = MessageDispatcher(user, target)

    if wrapper.public and botconfig.IGNORE_HIDDEN_COMMANDS and not chan.startswith(tuple(hooks.Features["CHANTYPES"])):
        return

    if (notice and ((wrapper.public and not botconfig.ALLOW_NOTICE_COMMANDS) or
                    (wrapper.private and not botconfig.ALLOW_PRIVATE_NOTICE_COMMANDS))):
        return  # not allowed in settings

    if force_role is None: # if force_role isn't None, that indicates recursion; don't fire these off twice
        for fn in decorators.COMMANDS[""]:
            fn.caller(cli, rawnick, ch, msg)

    parts = msg.split(sep=" ", maxsplit=1)
    key = parts[0].lower()
    if len(parts) > 1:
        message = parts[1].lstrip()
    else:
        message = ""

    if wrapper.public and not key.startswith(botconfig.CMD_CHAR):
        return # channel message but no prefix; ignore

    if key.startswith(botconfig.CMD_CHAR):
        key = key[len(botconfig.CMD_CHAR):]

    if not key: # empty key ("") already handled above
        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 user in get_participants():
        roles = get_all_roles(user)
        # A user can be a participant but not have a role, for example, dead vengeful ghost
        has_roles = len(roles) != 0
        if force_role is not None:
            roles &= {force_role} # only fire off role commands for the forced role

        common_roles = set(roles) # roles shared by every eligible role command
        have_role_cmd = False
        for fn in decorators.COMMANDS.get(key, []):
            if not fn.roles:
                cmds.append(fn)
                continue
            if roles.intersection(fn.roles):
                have_role_cmd = True
                cmds.append(fn)
                common_roles.intersection_update(fn.roles)

        if force_role is not None and not have_role_cmd:
            # Trying to force a non-role command with a role.
            # We allow non-role commands to execute if a role is forced if a role
            # command is also executed, as this would allow (for example) a bot admin
            # to add extra effects to all "kill" commands without needing to continually
            # update the list of roles which can use "kill". However, we don't want to
            # allow things like "wolf pstats" because that just doesn't make sense.
            return

        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]
            for fn in cmds:
                fn_roles = roles.intersection(fn.roles)
                if not fn_roles:
                    continue
                for role1 in common_roles:
                    info[0] = role1
                    break
                for role2 in fn_roles:
                    info[1] = role2
                    break
                common_roles &= fn_roles
                if not common_roles:
                    break
            wrapper.pm(messages["ambiguous_command"].format(key, info[0], info[1]))
            return
    elif force_role is None:
        cmds = decorators.COMMANDS.get(key, [])

    for fn in cmds:
        if phase == var.PHASE:
            # FIXME: pass in var, wrapper, message instead of cli, rawnick, chan, message
            fn.caller(cli, rawnick, ch, message)
Ejemplo n.º 14
0
def fwarn_set(var, wrapper, args):
    if args.help:
        wrapper.reply(messages["fwarn_set_syntax"])
        return

    warning = db.get_warning(args.id)
    if not warning:
        wrapper.reply(messages["fwarn_invalid_warning"])
        return

    if args.expires is not None:
        try:
            expires = _parse_expires(args.expires, warning["issued"])
        except ValueError:
            wrapper.reply(messages["fwarn_expiry_invalid"])
            return
    else:
        expires = warning["expires"]

    if args.reason is not None:
        reason = " ".join(args.reason).strip()
        if not reason:
            wrapper.reply(messages["fwarn_reason_invalid"])
            return
    else:
        # maintain existing reason if none was specified
        reason = warning["reason"]

    if args.notes is not None:
        notes = " ".join(args.notes).strip()
        if not notes:
            # empty notes unsets them
            notes = None
    else:
        # maintain existing notes if none were specified
        notes = warning["notes"]

    db.set_warning(args.id, expires, reason, notes)
    wrapper.reply(messages["fwarn_done"])

    if var.LOG_CHANNEL:
        changes = []
        if expires != warning["expires"]:
            oldexpiry = warning["expires"] if warning["expires"] else messages[
                "fwarn_log_set_noexpiry"]
            newexpiry = expires if expires else messages[
                "fwarn_log_set_noexpiry"]
            changes.append(messages["fwarn_log_set_expiry"].format(
                oldexpiry, newexpiry))
        if reason != warning["reason"]:
            changes.append(messages["fwarn_log_set_reason"].format(
                warning["reason"], reason))
        if notes != warning["notes"]:
            if warning["notes"]:
                changes.append(messages["fwarn_log_set_notes"].format(
                    warning["notes"], notes))
            else:
                changes.append(
                    messages["fwarn_log_set_notes_new"].format(notes))
        warning["changed_by"] = wrapper.source
        warning["changes"] = changes
        if changes:
            log_msg = messages["fwarn_log_set"].format(**warning)
            channels.get(var.LOG_CHANNEL).send(log_msg, prefix=var.LOG_PREFIX)
Ejemplo n.º 15
0
def fwarn_add(var, wrapper, args):
    if args.help:
        wrapper.reply(messages["fwarn_add_syntax"])
        return

    if args.account:
        target = args.nick
    else:
        m = users.complete_match(args.nick)
        if m:
            target = m.get()
        else:
            target = args.nick

    if args.points < 0:
        wrapper.reply(messages["fwarn_points_invalid"])
        return

    try:
        expires = _parse_expires(args.expires)
    except ValueError:
        wrapper.reply(messages["fwarn_expiry_invalid"])
        return

    sanctions = {}

    if args.stasis is not None:
        if args.stasis < 1:
            wrapper.reply(messages["fwarn_stasis_invalid"])
            return
        sanctions["stasis"] = args.stasis

    if args.deny is not None:
        normalized_cmds = set()
        for cmd in args.deny:
            for obj in COMMANDS[cmd]:
                normalized_cmds.add(obj.key)
        # don't allow the warn command to be denied
        # in-game commands bypass deny restrictions as well
        normalized_cmds.discard("warn")
        sanctions["deny"] = normalized_cmds

    if args.ban is not None:
        try:
            sanctions["tempban"] = _parse_expires(args.ban)
        except ValueError:
            try:
                sanctions["tempban"] = int(args.ban)
            except ValueError:
                wrapper.reply(messages["fwarn_tempban_invalid"])
                return

    reason = " ".join(args.reason).strip()

    if args.notes is not None:
        notes = " ".join(args.notes).strip()
    else:
        notes = None

    warn_id = add_warning(target, args.points, wrapper.source, reason, notes,
                          expires, sanctions)
    if not warn_id:
        wrapper.reply(messages["fwarn_cannot_add"])
        return

    wrapper.reply(messages["fwarn_added"].format(warn_id))
    # Log to ops/log channel (even if the warning was placed in that channel)
    if var.LOG_CHANNEL:
        log_reason = reason
        if notes is not None:
            log_reason += " | " + notes
        if expires is None:
            log_exp = messages["fwarn_log_add_noexpiry"]
        else:
            log_exp = messages["fwarn_log_add_expiry"].format(expires)
        log_msg = messages["fwarn_log_add"].format(warn_id, target,
                                                   wrapper.source, log_reason,
                                                   args.points, log_exp)
        channels.get(var.LOG_CHANNEL).send(log_msg, prefix=var.LOG_PREFIX)