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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)