diff --git a/messages/en.json b/messages/en.json index dec35d5..1f0060e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -574,6 +574,7 @@ "account_not_in_stasis": "\u0002{0}\u0002 (Account: {1}) is not in stasis.", "currently_stasised": "Currently stasised: {0}", "noone_stasised": "Nobody is currently stasised.", + "stasis_cannot_increase": "Cannot increase stasis using fstasis; use fwarn instead.", "no_command_specified": "Error: No command specified. Did you mean \u0002-cmds\u0002?", "invalid_option": "Invalid option: {0}", "command_does_not_exist": "That command does not exist.", @@ -797,14 +798,20 @@ "villagergame_nope": "Game over! The villagers decided incorrectly that there are actually no wolves, allowing the wolves to slaughter the remainder of them in their sleep with impunity.", "stop_bot_ingame_safeguard": "Warning: A game is currently running. If you want to {what} the bot anyway, use \"{prefix}{cmd} -force\".", "fwarn_usage": "Usage: fwarn list|view|add|del|set|help. See fwarn help for more details.", + "warn_usage": "Usage: warn list|view|ack|help. See warn help for more details.", "fwarn_list_syntax": "Usage: fwarn list [-all] [nick[!user@host]|=account] [page]", "fwarn_view_syntax": "Usage: fwarn view ", "fwarn_del_syntax": "Usage: fwarn del ", "fwarn_set_syntax": "Usage: fwarn set [| notes]", "fwarn_help_syntax": "Usage: fwarn help ", + "warn_list_syntax": "Usage: warn list [-all] [page]", + "warn_view_syntax": "Usage: warn view ", + "warn_ack_syntax": "Usage: warn ack ", + "warn_help_syntax": "Uwage: warn help ", "fwarn_add_syntax": "Usage: fwarn add [@] [~expiry] [sanctions] <:reason> [| notes]", "fwarn_page_invalid": "Invalid page, must be a number 1 or greater.", "fwarn_points_invalid": "Invalid points, must be a number above 0.", + "fwarn_expiry_invalid": "Invalid expiration amount, must be a number above 0.", "fwarn_expiry_invalid_suffix": "Invalid expiration suffix, must use either d, h, or m.", "fwarn_cannot_add": "Cannot add warning, double-check your parameters (the nick might be wrong or you are not joined to the channel).", "fwarn_added": "Added warning {0}.", @@ -813,24 +820,32 @@ "fwarn_stasis_invalid": "Invalid stasis amount, specify sanction as stasis=number.", "fwarn_deny_invalid": "Invalid denied commands, specify sanction as deny=command,command,command without spaces.", "fwarn_deny_invalid_command": "Invalid command \"{0}\", specify sanction as deny=command,command,command without spaces.", - "fwarn_list_header": "{0} has {1} active warning points.", + "fwarn_list_header": "{0} has {1} active warning points. Warnings prefixed with \u0002!\u0002 are unacknowledged.", + "warn_list_header": "You have {0} active warning points. You must acknowledge all warnings prefixed with \u0002!\u0002 by using \"warn ack \" before you can join games.", "fwarn_list": "{0}{1}[#{2} {3}] to {4} by {5} - {6} ({7} points, {8}){9}", + "warn_list": "{0}{1}[#{2} {3}] {4} ({5} points, {6}){7}", "fwarn_deleted": "deleted", "fwarn_expired": "expired", + "fwarn_list_expired": "expired on {0}", "fwarn_never_expires": "never expires", "fwarn_list_footer": "More results are available, use fwarn list {0} to view them.", + "warn_list_footer": "More results are available, use warn list {0} to view them.", "fwarn_list_empty": "No results.", "fwarn_invalid_warning": "The specified warning id does not exist or you do not have permission to view it.", "fwarn_view_header": "Warning #{0}, given to {1} on {2} by {3}. {4} points. {5}.", + "warn_view_header": "Warning #{0}, given on {1}. {2} points. {3}.", "fwarn_view_active": "Currently active, {0}", "fwarn_view_expires": "expires on {0}", "fwarn_view_expired": "Expired on {0}", "fwarn_view_deleted": "Deleted on {0} by {1}", "fwarn_view_ack": "Warning has not yet been acknowledged.", + "warn_view_ack": "You have not yet acknowledge this warning. You must acknowledge this warning by using \"warn ack {0}\" before you can join games.", "fwarn_view_sanctions": "Sanctions:", - "fwarn_view_stasis": "{0} games of stasis.", + "fwarn_view_stasis_sing": "1 game of stasis.", + "fwarn_view_stasis_plural": "{0} games of stasis.", "fwarn_view_deny": "denied {0}.", "fwarn_reason_required": "A public warning reason is required.", + "warn_unacked": "You have unacknowledged warnings and cannot join at this time. Use \"warn list\" to view them.", "_": " vim: set sw=4 expandtab:" } diff --git a/src/__init__.py b/src/__init__.py index ac53da1..f011911 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -84,10 +84,6 @@ if args.normal: normal = True botconfig.DEBUG_MODE = debug_mode if not normal else False botconfig.VERBOSE_MODE = verbose if not normal else False -# Initialize Database - -db.init() - # Logger # replace characters that can't be encoded with '?' diff --git a/src/db.py b/src/db.py index 38e4fc1..eae9dd5 100644 --- a/src/db.py +++ b/src/db.py @@ -3,34 +3,37 @@ import src.settings as var import sqlite3 import os import json +from collections import defaultdict # increment this whenever making a schema change so that the schema upgrade functions run on start # they do not run by default for performance reasons SCHEMA_VERSION = 1 -conn = None +need_install = not os.path.isfile("data.sqlite3") +conn = sqlite3.connect("data.sqlite3") +with conn: + c = conn.cursor() + c.execute("PRAGMA foreign_keys = ON") + if need_install: + _install() + c.execute("PRAGMA user_version") + row = c.fetchone() + if row[0] == 0: + # new schema does not exist yet, migrate from old schema + # NOTE: game stats are NOT migrated to the new schema; the old gamestats table + # will continue to exist to allow queries against it, however given how horribly + # inaccurate the stats on it are, it would be a disservice to copy those inaccurate + # statistics over to the new schema which has the capability of actually being accurate. + _migrate() + elif row[0] < SCHEMA_VERSION: + _upgrade() + c.close() -def init(): - global conn - need_install = not os.path.isfile("data.sqlite3") - conn = sqlite3.connect("data.sqlite3") - with conn: +del need_install, c + +def init_vars(): + with var.GRAVEYARD_LOCK: c = conn.cursor() - c.execute("PRAGMA foreign_keys = ON") - if need_install: - _install() - c.execute("PRAGMA user_version") - row = c.fetchone() - if row[0] == 0: - # new schema does not exist yet, migrate from old schema - # NOTE: game stats are NOT migrated to the new schema; the old gamestats table - # will continue to exist to allow queries against it, however given how horribly - # inaccurate the stats on it are, it would be a disservice to copy those inaccurate - # statistics over to the new schema which has the capability of actually being accurate. - _migrate() - elif row[0] < SCHEMA_VERSION: - _upgrade() - c.execute("""SELECT pl.account, pl.hostmask, @@ -39,14 +42,37 @@ def init(): pe.deadchat, pe.pingif, pe.stasis_amount, - pe.stasis_expires + pe.stasis_expires, + COALESCE(at.flags, a.flags) FROM person pe JOIN person_player pp ON pp.person = pe.id JOIN player pl ON pl.id = pp.player + LEFT JOIN access a + ON a.person = pe.id + LEFT JOIN access_template at + ON at.id = a.template WHERE pl.active = 1""") - for (acc, host, notice, simple, dc, pi, stasis, stasisexp) in c: + + var.SIMPLE_NOTIFY = set() # cloaks of people who !simple, who don't want detailed instructions + var.SIMPLE_NOTIFY_ACCS = set() # same as above, except accounts. takes precedence + var.PREFER_NOTICE = set() # cloaks of people who !notice, who want everything /notice'd + var.PREFER_NOTICE_ACCS = set() # Same as above, except accounts. takes precedence + var.STASISED = defaultdict(int) + var.STASISED_ACCS = defaultdict(int) + var.PING_IF_PREFS = {} + var.PING_IF_PREFS_ACCS = {} + var.PING_IF_NUMS = defaultdict(set) + var.PING_IF_NUMS_ACCS = defaultdict(set) + var.DEADCHAT_PREFS = set() + var.DEADCHAT_PREFS_ACCS = set() + var.FLAGS = defaultdict(str) + var.FLAGS_ACCS = defaultdict(str) + var.DENY = defaultdict(set) + var.DENY_ACCS = defaultdict(set) + + for acc, host, notice, simple, dc, pi, stasis, stasisexp, flags in c: if acc is not None: if simple == 1: var.SIMPLE_NOTIFY_ACCS.add(acc) @@ -59,6 +85,8 @@ def init(): var.PING_IF_NUMS_ACCS[pi].add(acc) if dc == 1: var.DEADCHAT_PREFS_ACCS.add(acc) + if flags: + var.FLAGS_ACCS[acc] = flags elif host is not None: if simple == 1: var.SIMPLE_NOTIFY.add(host) @@ -71,6 +99,74 @@ def init(): var.PING_IF_NUMS[pi].add(host) if dc == 1: var.DEADCHAT_PREFS.add(host) + if flags: + var.FLAGS[host] = flags + + c.execute("""SELECT + pl.account, + pl.hostmask, + ws.data + FROM warning w + JOIN warning_sanction ws + ON ws.warning = w.id + JOIN person pe + ON pe.id = w.target + JOIN person_player pp + ON pp.person = pe.id + JOIN player pl + ON pl.id = pp.player + WHERE + ws.sanction = 'deny command' + AND w.deleted = 0 + AND ( + w.expires IS NULL + OR w.expires > datetime('now') + )""") + for acc, host, command in c: + if acc is not None: + var.DENY_ACCS[acc].add(command) + if host is not None: + var.DENY[host].add(command) + +init_vars() + +def decrement_stasis(acc=None, hostmask=None): + peid, plid = _get_ids(acc, hostmask) + if (acc is not None or hostmask is not None) and peid is None: + return + sql = "UPDATE person SET stasis_amount = MAX(0, stasis_amount - 1)" + params = () + if peid is not None: + sql += " WHERE id = ?" + params = (peid,) + + with conn: + c = conn.cursor() + c.execute(sql, params) + +def decrease_stasis(newamt, acc=None, hostmask=None): + peid, plid = _get_ids(acc, hostmask) + if peid is None: + return + if newamt < 0: + newamt = 0 + + with conn: + c = conn.cursor() + c.execute("""UPDATE person + SET stasis_amount = MIN(stasis_amount, ?) + WHERE id = ?""", (newamt, peid)) + +def expire_stasis(): + with conn: + c = conn.cursor() + c.execute("""UPDATE person + SET + stasis_amount = 0, + stasis_expires = NULL + WHERE + stasis_expires IS NOT NULL + AND stasis_expires <= datetime('now')""") def toggle_simple(acc, hostmask): _toggle_thing("simple", acc, hostmask) @@ -84,9 +180,6 @@ def toggle_deadchat(acc, hostmask): def set_pingif(val, acc, hostmask): _set_thing("pingif", val, acc, hostmask, raw=False) -def set_stasis(val, acc, hostmask): - _set_thing("stasis_amount", val, acc, hostmask, raw=False) - def add_game(mode, size, started, finished, winner, players, options): """ Adds a game record to the database. @@ -257,39 +350,6 @@ def get_game_totals(mode): totals.append("\u0002{0}p\u0002: {1}".format(*row)) return "Total games ({0}) | {1}".format(total_games, ", ".join(totals)) -def get_flags(acc, hostmask): - peid, plid = _get_ids(acc, hostmask) - c = conn.cursor() - c.execute("""SELECT COALESCE(at.flags, a.flags) - FROM access a - LEFT JOIN access_template at - ON at.id = a.template - WHERE a.person = ?""", (peid,)) - row = c.fetchone() - if row is None: - return "" - return row[0] - -def get_denied_commands(acc, hostmask): - peid, plid = _get_ids(acc, hostmask) - c = conn.cursor() - c.execute("""SELECT ws.data - FROM warning w - JOIN warning_sanction ws - ON ws.warning = w.id - WHERE - ws.sanction = 'deny command' - AND w.target = ? - AND w.deleted = 0 - AND ( - w.expires IS NULL - OR w.expires > datetime('now') - )""", (peid,)) - cmds = set() - for row in c: - cmds.add(row[0]) - return cmds - def get_warning_points(acc, hostmask): peid, plid = _get_ids(acc, hostmask) c = conn.cursor() @@ -305,6 +365,23 @@ def get_warning_points(acc, hostmask): row = c.fetchone() return row[0] +def has_unacknowledged_warnings(acc, hostmask): + peid, plid = _get_ids(acc, hostmask) + if peid is None: + return False + c = conn.cursor() + c.execute("""SELECT MIN(acknowledged) + FROM warning + WHERE + target = ? + AND deleted = 0 + AND ( + expires IS NULL + OR expires > datetime('now') + )""", (peid,)) + row = c.fetchone() + return not bool(row[0]) + def list_all_warnings(list_all=False, skip=0, show=0): c = conn.cursor() sql = """SELECT @@ -356,7 +433,7 @@ def list_all_warnings(list_all=False, skip=0, show=0): "reason": row[9]}) return warnings -def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): +def list_warnings(acc, hostmask, expired=False, deleted=False, skip=0, show=0): peid, plid = _get_ids(acc, hostmask) c = conn.cursor() sql = """SELECT @@ -383,16 +460,16 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): WHERE warning.target = ? """ - if not list_all: - sql += """ AND deleted = 0 - AND ( + if not deleted: + sql += " AND deleted = 0" + if not expired: + sql += """ AND ( expires IS NULL OR expires > datetime('now') - ) - """ - sql += "ORDER BY warning.issued DESC\n" + )""" + sql += " ORDER BY warning.issued DESC" if show > 0: - sql += "LIMIT {0} OFFSET {1}".format(show, skip) + sql += " LIMIT {0} OFFSET {1}".format(show, skip) c.execute(sql, (botconfig.NICK, peid)) warnings = [] @@ -410,7 +487,7 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): return warnings def get_warning(warn_id, acc=None, hm=None): - pe, pl = _get_ids(acc, hm) + peid, plid = _get_ids(acc, hm) c = conn.cursor() sql = """SELECT warning.id, @@ -486,13 +563,14 @@ def get_warning_sanctions(warn_id): def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack): teid, tlid = _get_ids(tacc, thm) seid, slid = _get_ids(sacc, shm) + ack = 0 if need_ack else 1 with conn: c = conn.cursor() c.execute("""INSERT INTO warning ( target, sender, amount, issued, expires, - reasons, notes, + reason, notes, acknowledged ) VALUES @@ -501,7 +579,7 @@ def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack): datetime('now'), ?, ?, ?, ? - )""", (teid, seid, amount, expires, reasons, notes, not need_ack)) + )""", (teid, seid, amount, expires, reason, notes, ack)) return c.lastrowid def add_warning_sanction(warning, sanction, data): @@ -512,6 +590,19 @@ def add_warning_sanction(warning, sanction, data): VALUES (?, ?, ?)""", (warning, sanction, data)) + if sanction == "stasis": + c.execute("SELECT target FROM warning WHERE id = ?", (warning,)) + peid = c.fetchone()[0] + c.execute("""UPDATE person + SET + stasis_amount = stasis_amount + ?, + stasis_expires = datetime(CASE WHEN stasis_expires IS NULL + OR stasis_expires <= datetime('now') + THEN 'now' + ELSE stasis_expires END, + '+{0} hours') + WHERE id = ?""".format(int(data)), (data, peid)) + def del_warning(warning, acc, hm): peid, plid = _get_ids(acc, hm) with conn: @@ -533,6 +624,11 @@ def set_warning(warning, reason, notes): SET reason = ?, notes = ? WHERE id = ?""", (reason, notes, warning)) +def acknowledge_warning(warning): + with conn: + c = conn.cursor() + c.execute("UPDATE warning SET acknowledged = 1 WHERE id = ?", (warning,)) + def _upgrade(): # no upgrades yet, once there are some, add methods like _add_table(), _add_column(), etc. # that check for the existence of that table/column/whatever and adds/drops/whatevers them diff --git a/src/decorators.py b/src/decorators.py index 853b286..ac3af7d 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -188,15 +188,13 @@ class cmd: cli.notice(nick, messages["not_owner"]) return - # TODO: cache flags and cmds (below) on init, possibly store in var.USERS - # that would greatly reduce our db calls - flags = db.get_flags(acc, hostmask) + flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc] is_full_admin = "F" in flags if self.flag and (is_full_admin or is_owner): adminlog(chan, rawnick, self.name, rest) return self.func(*largs) - denied_cmds = db.get_denied_commands(acc, hostmask) + denied_cmds = var.DENY[hostmask] | var.DENY_ACCS[acc] for command in self.cmds: if command in denied_cmds: if chan == nick: diff --git a/src/settings.py b/src/settings.py index cdb0701..3376704 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,5 +1,6 @@ import fnmatch import re +import threading from collections import defaultdict, OrderedDict import botconfig @@ -195,10 +196,6 @@ TOTEM_CHANCES = { "death": ( 1 , 1 , 0 GAME_MODES = {} GAME_PHASES = ("night", "day") # all phases that constitute "in game", game modes can extend this with custom phases -SIMPLE_NOTIFY = set() # cloaks of people who !simple, who don't want detailed instructions -SIMPLE_NOTIFY_ACCS = set() # same as above, except accounts. takes precedence -PREFER_NOTICE = set() # cloaks of people who !notice, who want everything /notice'd -PREFER_NOTICE_ACCS = set() # Same as above, except accounts. takes precedence ACCOUNTS_ONLY = False # If True, will use only accounts for everything DISABLE_ACCOUNTS = False # If True, all account-related features are disabled. Automatically set if we discover we do not have proper ircd support for accounts @@ -212,9 +209,6 @@ NICKSERV_REGAIN_COMMAND = "REGAIN {nick}" CHANSERV = "ChanServ" CHANSERV_OP_COMMAND = "OP {channel}" -STASISED = defaultdict(int) -STASISED_ACCS = defaultdict(int) - # TODO: move this to a game mode called "fixed" once we implement a way to randomize roles (and have that game mode be called "random") DEFAULT_ROLE = "villager" ROLE_INDEX = ( 4 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 15 , 16 , 18 , 20 , 21 , 23 , 24 ) @@ -328,16 +322,9 @@ FORTUNE_CHANCE = 1/25 RULES = (botconfig.CHANNEL + " channel rules: http://wolf.xnrand.com/rules") -# pingif-related mappings - -PING_IF_PREFS = {} -PING_IF_PREFS_ACCS = {} - -PING_IF_NUMS = defaultdict(set) -PING_IF_NUMS_ACCS = defaultdict(set) - -DEADCHAT_PREFS = set() -DEADCHAT_PREFS_ACCS = set() +GRAVEYARD_LOCK = threading.RLock() +WARNING_LOCK = threading.RLock() +WAIT_TB_LOCK = threading.RLock() #TODO: move all of these to util.py or other files, as they are certainly NOT settings! @@ -386,7 +373,7 @@ def is_admin(nick, ident=None, host=None, acc=None): if not acc: acc = USERS[nick]["account"] hostmask = nick + "!" + ident + "@" + host - flags = db.get_flags(acc, hostmask) + flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc] return "F" in flags def irc_lower(nick): diff --git a/src/wolfgame.py b/src/wolfgame.py index ccc39fc..3495993 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -89,9 +89,6 @@ var.LAST_SAID_TIME = {} var.GAME_START_TIME = datetime.now() # for idle checker only var.CAN_START_TIME = 0 -var.GRAVEYARD_LOCK = threading.RLock() -var.WARNING_LOCK = threading.RLock() -var.WAIT_TB_LOCK = threading.RLock() var.STARTED_DAY_PLAYERS = 0 var.DISCONNECTED = {} # players who got disconnected @@ -361,6 +358,15 @@ def get_victim(cli, nick, victim, in_chan, self_in_list=False, bot_in_list=False return return pl[pll.index(tempvictim)] #convert back to normal casing +# wrapper around complete_match() used for any nick on the channel +def get_nick(cli, nick): + ul = [x for x in var.USERS] + ull = [x.lower() for x in var.USERS] + lnick, num_matches = complete_match(nick.lower(), ull) + if not lnick: + return None + return ul[ull.index(lnick)] + def get_roles(*roles): all_roles = [] for role in roles: @@ -449,6 +455,10 @@ def sync_modes(cli): mass_mode(cli, voices, other) +@cmd("refreshdb", flag="m", pm=True) +def refreshdb(cli, nick, chan, rest): + """Updates our tracking vars to the current db state.""" + db.init_vars() @cmd("fdie", "fbye", flag="D", pm=True) def forced_exit(cli, nick, chan, rest): @@ -1162,11 +1172,13 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): ident = var.USERS[player]["ident"] host = var.USERS[player]["host"] acc = var.USERS[player]["account"] + hostmask = player + "!" + ident + "@" + host elif is_fake_nick(player) and botconfig.DEBUG_MODE: # fakenick ident = None host = None acc = None + hostmask = None else: return False # Not normal if not acc or acc == "*" or var.DISABLE_ACCOUNTS: @@ -1183,6 +1195,11 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): "s" if stasis != 1 else "")) return False + # don't check unacked warnings on fjoin + if who == player and db.has_unacknowledged_warnings(acc, hostmask): + cli.notice(player, messages["warn_unacked"]) + return False + cmodes = [("+v", player)] if var.PHASE == "none": if var.AUTO_TOGGLE_MODES and player in var.USERS and var.USERS[player]["modes"]: @@ -1297,6 +1314,9 @@ def kill_join(cli, chan): reset() cli.msg(chan, msg) cli.msg(chan, messages["game_idle_cancel"]) + # use this opportunity to expire pending stasis + db.expire_stasis() + db.init_vars() if var.AFTER_FLASTGAME is not None: var.AFTER_FLASTGAME() var.AFTER_FLASTGAME = None @@ -7907,19 +7927,15 @@ def decrement_stasis(nick=None): acc = var.USERS[nick]["account"] # decrement account stasis even if accounts are disabled if acc in var.STASISED_ACCS: - var.STASISED_ACCS[acc] = max(0, var.STASISED_ACCS[acc] - 1) - db.set_stasis(var.STASISED_ACCS[acc], acc, None) + db.decrement_stasis(acc=acc) for hostmask in var.STASISED: if var.match_hostmask(hostmask, nick, ident, host): - var.STASISED[hostmask] = max(0, var.STASISED[hostmask] - 1) - db.set_stasis(var.STASISED[hostmask], None, hostmask) + db.decrement_stasis(hostmask=hostmask) else: - for acc in var.STASISED_ACCS: - var.STASISED_ACCS[acc] = max(0, var.STASISED_ACCS[acc] - 1) - db.set_stasis(var.STASISED_ACCS[acc], acc, None) - for hostmask in var.STASISED: - var.STASISED[hostmask] = max(0, var.STASISED[hostmask] - 1) - db.set_stasis(var.STASISED[hostmask], None, hostmask) + db.decrement_stasis() + # Also expire any expired stasis and update our tracking vars + db.expire_stasis() + db.init_vars() def parse_warning_target(target): if target[0] == "=": @@ -7988,6 +8004,9 @@ def add_warning(target, amount, actor, reason, notes=None, expires=None, need_ac for cmd in sanctions["deny"]: db.add_warning_sanction(sid, "deny command", cmd) + # Update any tracking vars that may have changed due to this + db.init_vars() + return sid @cmd("stasis", chan=True, pm=True) @@ -8000,14 +8019,226 @@ def stasis(cli, nick, chan, rest): reply(cli, nick, chan, msg, prefix_nick=True) +@cmd("fstasis", flag="A", chan=True, pm=True) +def fstasis(cli, nick, chan, rest): + """Removes or views stasis penalties.""" + + data = rest.split() + msg = None + + if data: + lusers = {k.lower(): v for k, v in var.USERS.items()} + acc, hostmask = parse_warning_target(data[0]) + cur = max(var.STASISED[hostmask], var.STASISED_ACCS[acc]) + + if len(data) == 1: + if acc is not None and var.STASISED_ACCS[acc] == cur: + plural = "" if cur == 1 else "s" + reply(cli, nick, chan, messages["account_in_stasis"].format(data[0], acc, cur, plural)) + elif hostmask is not None and var.STASISED[hostmask] == cur: + plural = "" if cur == 1 else "s" + reply(cli, nick, chan, messages["hostmask_in_stasis"].format(data[0], hostmask, cur, plural)) + elif acc is not None: + reply(cli, nick, chan, messages["account_not_in_stasis"].format(data[0], acc)) + else: + reply(cli, nick, chan, messages["hostmask_not_in_stasis"].format(data[0], hostmask)) + else: + try: + amt = int(data[1]) + except ValueError: + reply(cli, nick, chan, messages["stasis_not_negative"]) + return + + if amt < 0: + reply(cli, nick, chan, messages["stasis_not_negative"]) + return + elif amt > cur: + reply(cli, nick, chan, messages["stasis_cannot_increase"]) + return + elif cur == 0: + if acc is not None: + reply(cli, nick, chan, messages["account_not_in_stasis"].format(data[0], acc)) + return + else: + reply(cli, nick, chan, messages["hostmask_not_in_stasis"].format(data[0], hostmask)) + return + + db.decrease_stasis(amt, acc, hostmask) + db.init_vars() + if amt > 0: + plural = "" if amt == 1 else "s" + if acc is not None: + reply(cli, nick, chan, messages["fstasis_account_add"].format(data[0], acc, amt, plural)) + else: + reply(cli, nick, chan, messages["fstasis_hostmask_add"].format(data[0], hostmask, amt, plural)) + elif acc is not None: + reply(cli, nick, chan, messages["fstasis_account_remove"].format(data[0], acc)) + else: + reply(cli, nick, chan, messages["fstasis_hostmask_remove"].format(data[0], hostmask)) + elif var.STASISED or var.STASISED_ACCS: + stasised = {} + for hostmask in var.STASISED: + if var.DISABLE_ACCOUNTS: + stasised[hostmask] = var.STASISED[hostmask] + else: + stasised[hostmask+" (Host)"] = var.STASISED[hostmask] + if not var.DISABLE_ACCOUNTS: + for acc in var.STASISED_ACCS: + stasised[acc+" (Account)"] = var.STASISED_ACCS[acc] + msg = messages["currently_stasised"].format(", ".join( + "\u0002{0}\u0002 ({1})".format(usr, number) + for usr, number in stasised.items())) + reply(cli, nick, chan, msg) + else: + reply(cli, nick, chan, messages["noone_stasised"]) + @cmd("warn", pm=True) def warn(cli, nick, chan, rest): """View and acknowledge your warnings.""" - # !warn list [all] - lists all active warnings, or all warnings if all passed + # !warn list [-all] [page] - lists all active warnings, or all warnings if all passed # !warn view - views details on warning id # !warn ack - acknowledges warning id # Default if only !warn is given is to do !warn list. - pass + params = re.split(" +", rest) + + try: + command = params.pop(0) + if command == "": + command = "list" + except IndexError: + command = "list" + + if command not in ("list", "view", "ack", "help"): + reply(cli, nick, chan, messages["warn_usage"]) + return + + if command == "help": + try: + subcommand = params.pop(0) + except IndexError: + reply(cli, nick, chan, messages["warn_help_syntax"]) + return + if subcommand not in ("list", "view", "ack", "help"): + reply(cli, nick, chan, messages["warn_usage"]) + return + reply(cli, nick, chan, messages["warn_{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: + reply(cli, nick, chan, messages["fwarn_page_invalid"]) + return + + try: + if list_all and list_all != "-all": + page = int(list_all) + list_all = False + elif list_all == "-all": + list_all = True + except ValueError: + reply(cli, nick, chan, messages["fwarn_page_invalid"]) + return + + acc, hm = parse_warning_target(nick) + warnings = db.list_warnings(acc, hm, expired=list_all, skip=(page-1)*10, show=11) + points = db.get_warning_points(acc, hm) + cli.notice(nick, messages["warn_list_header"].format(points)) + + i = 0 + for warn in warnings: + i += 1 + if (i == 11): + parts = [] + if list_all: + parts.append("-all") + parts.append(str(page + 1)) + cli.notice(nick, messages["warn_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["expired"]: + start = "\u000314" + end = " [\u00037{0}\u000314]\u0003".format(messages["fwarn_expired"]) + if not warn["ack"]: + ack = "\u0002!\u0002 " + cli.notice(nick, messages["warn_list"].format( + start, ack, warn["id"], warn["issued"], warn["reason"], warn["amount"], expires, end)) + if i == 0: + cli.notice(nick, messages["fwarn_list_empty"]) + return + + if command == "view": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["warn_view_syntax"]) + return + + acc, hm = parse_warning_target(nick) + warning = db.get_warning(warn_id, acc, hm) + if warning is None: + reply(cli, nick, chan, messages["fwarn_invalid_warning"]) + return + + if 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"])) + + cli.notice(nick, messages["warn_view_header"].format( + warning["id"], warning["issued"], warning["amount"], expires)) + cli.notice(nick, warning["reason"]) + + sanctions = [] + if not warning["ack"]: + sanctions.append(messages["warn_view_ack"].format(warning["id"])) + 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 sanctions: + cli.notice(nick, " ".join(sanctions)) + return + + if command == "ack": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["warn_ack_syntax"]) + return + + acc, hm = parse_warning_target(nick) + warning = db.get_warning(warn_id, acc, hm) + if warning is None: + reply(cli, nick, chan, messages["fwarn_invalid_warning"]) + return + + db.acknowledge_warning(warn_id) + reply(cli, nick, chan, messages["fwarn_done"]) + return @cmd("fwarn", flag="A", pm=True) def fwarn(cli, nick, chan, rest): @@ -8039,7 +8270,7 @@ def fwarn(cli, nick, chan, rest): target = None points = None need_ack = False - expiry = None + expires = None sanctions = {} reason = None notes = None @@ -8050,6 +8281,10 @@ def fwarn(cli, nick, chan, rest): reply(cli, nick, chan, messages["fwarn_usage"]) return + if command not in ("list", "view", "add", "del", "set", "help"): + reply(cli, nick, chan, messages["fwarn_usage"]) + return + if command == "help": try: subcommand = params.pop(0) @@ -8069,18 +8304,30 @@ def fwarn(cli, nick, chan, rest): list_all = params.pop(0) target = params.pop(0) page = int(params.pop(0)) - if list_all and list_all != "-all": - if target is not None: - page = int(target) - target = list_all - list_all = False - elif show_all == "-all": - list_all = True except IndexError: pass except ValueError: reply(cli, nick, chan, 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: + reply(cli, nick, chan, 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: @@ -8088,7 +8335,7 @@ def fwarn(cli, nick, chan, rest): return warnings = db.list_warnings(acc, hm, list_all=list_all, skip=(page-1)*10, show=11) points = db.get_warning_points(acc, hm) - reply(cli, nick, chan, messages["fwarn_list_header"].format(target, points)) + cli.notice(nick, messages["fwarn_list_header"].format(target, points)) else: warnings = db.list_all_warnings(list_all=list_all, skip=(page-1)*10, show=11) @@ -8102,12 +8349,18 @@ def fwarn(cli, nick, chan, rest): if target is not None: parts.append(target) parts.append(str(page + 1)) - reply(cli, nick, chan, messages["fwarn_list_footer"].format(" ".join(parts))) + cli.notice(nick, messages["fwarn_list_footer"].format(" ".join(parts))) break start = "" end = "" ack = "" - expires = warn["expires"] if warn["expires"] is not None else messages["fwarn_never_expires"] + 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"]) @@ -8116,11 +8369,11 @@ def fwarn(cli, nick, chan, rest): end = " [\u00037{0}\u000314]\u0003".format(messages["fwarn_expired"]) if not warn["ack"]: ack = "\u0002!\u0002 " - reply(cli, nick, chan, messages["fwarn_list"].format( + cli.notice(nick, messages["fwarn_list"].format( start, ack, warn["id"], warn["issued"], warn["target"], warn["sender"], warn["reason"], warn["amount"], expires, end)) if i == 0: - reply(cli, nick, chan, messages["fwarn_list_empty"]) + cli.notice(nick, messages["fwarn_list_empty"]) return if command == "view": @@ -8144,14 +8397,14 @@ def fwarn(cli, nick, chan, rest): else: expires = messages["fwarn_view_active"].format(messages["fwarn_view_expires"].format(warning["expires"])) - reply(cli, nick, chan, messages["fwarn_view_header"].format( + cli.notice(nick, messages["fwarn_view_header"].format( warning["id"], warning["target"], warning["issued"], warning["sender"], warning["amount"], expires)) reason = [warning["reason"]] if warning["notes"] is not None: reason.append(warning["notes"]) - reply(cli, nick, chan, " | ".join(reason)) + cli.notice(nick, " | ".join(reason)) sanctions = [] if not warning["ack"]: @@ -8159,11 +8412,14 @@ def fwarn(cli, nick, chan, rest): if warning["sanctions"]: sanctions.append(messages["fwarn_view_sanctions"]) if "stasis" in warning["sanctions"]: - sanctions.append(messages["fwarn_view_stasis"].format(warning["sanctions"]["stasis"])) + 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 sanctions: - reply(cli, nick, chan, " ".join(sanctions)) + cli.notice(nick, " ".join(sanctions)) return if command == "del": @@ -8217,10 +8473,6 @@ def fwarn(cli, nick, chan, rest): reply(cli, nick, chan, messages["fwarn_done"]) return - if command != "add": - reply(cli, nick, chan, messages["fwarn_usage"]) - return - # command == "add" while params: p = params.pop(0) @@ -8256,7 +8508,7 @@ def fwarn(cli, nick, chan, rest): if p == "~": reply(cli, nick, chan, messages["fwarn_syntax"]) return - expiry = p[1:] + expires = p[1:] else: # sanctions are the only thing left here sanc = p.split("=", 1) @@ -8303,12 +8555,22 @@ def fwarn(cli, nick, chan, rest): # convert expires into a proper datetime if expires is not None: suffix = expires[-1] + try: + amount = int(expires[:-1]) + except ValueError: + reply(cli, nick, chan, messages["fwarn_expiry_invalid"]) + return + + if amount <= 0: + reply(cli, nick, chan, messages["fwarn_expiry_invalid"]) + return + if suffix == "d": - expires = datetime.now() + timedelta(days=expires[:-1]) + expires = datetime.now() + timedelta(days=amount) elif suffix == "h": - expires = datetime.now() + timedelta(hours=expires[:-1]) + expires = datetime.now() + timedelta(hours=amount) elif suffix == "m": - expires = datetime.now() + timedelta(minutes=expires[:-1]) + expires = datetime.now() + timedelta(minutes=amount) else: reply(cli, nick, chan, messages["fwarn_expiry_invalid_suffix"]) return