diff --git a/messages/en.json b/messages/en.json index b5aeb60..dec35d5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -796,27 +796,41 @@ "villagergame_win": "Game over! The villagers come to their senses and realize there are actually no wolves, and live in harmony forevermore. Everybody wins.", "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|help. See fwarn help for more details.", + "fwarn_usage": "Usage: fwarn list|view|add|del|set|help. See fwarn 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_del_syntax": "Usage: fwarn del ", + "fwarn_set_syntax": "Usage: fwarn set [| notes]", "fwarn_help_syntax": "Usage: fwarn help ", - "fwarn_add_syntax": "Usage: fwarn add [@] [~expiry] [sanctions] <:reason> [|notes]", + "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_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}.", + "fwarn_done": "Done.", "fwarn_sanction_invalid": "Invalid sanction, can be either deny or stasis.", "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": "{0}{1}[#{2} {3}] {4} by {5} - {6} ({7}, {8}){9}", + "fwarn_list": "{0}{1}[#{2} {3}] to {4} by {5} - {6} ({7} points, {8}){9}", "fwarn_deleted": "deleted", "fwarn_expired": "expired", "fwarn_never_expires": "never expires", "fwarn_list_footer": "More results are available, use fwarn 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}.", + "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.", + "fwarn_view_sanctions": "Sanctions:", + "fwarn_view_stasis": "{0} games of stasis.", + "fwarn_view_deny": "denied {0}.", + "fwarn_reason_required": "A public warning reason is required.", "_": " vim: set sw=4 expandtab:" } diff --git a/src/db.py b/src/db.py index 29a052c..38e4fc1 100644 --- a/src/db.py +++ b/src/db.py @@ -1,3 +1,4 @@ +import botconfig import src.settings as var import sqlite3 import os @@ -265,6 +266,8 @@ def get_flags(acc, hostmask): 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): @@ -312,17 +315,18 @@ def list_all_warnings(list_all=False, skip=0, show=0): warning.issued, warning.expires, CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') - THEN 1 ELSE 0 END AS expired, + THEN 0 ELSE 1 END AS expired, warning.acknowledged, warning.deleted, warning.reason + FROM warning JOIN person pet ON pet.id = warning.target JOIN player plt ON plt.id = pet.primary_player - LEFT JOIN pes + LEFT JOIN person pes ON pes.id = warning.sender - LEFT JOIN pls + LEFT JOIN player pls ON pls.id = pes.primary_player """ if not list_all: @@ -348,8 +352,8 @@ def list_all_warnings(list_all=False, skip=0, show=0): "expires": row[5], "expired": row[6], "ack": row[7], - "deleted": row[8]}, - "reason": row[9]) + "deleted": row[8], + "reason": row[9]}) return warnings def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): @@ -363,17 +367,18 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): warning.issued, warning.expires, CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') - THEN 1 ELSE 0 END AS expired, + THEN 0 ELSE 1 END AS expired, warning.acknowledged, warning.deleted, warning.reason + FROM warning JOIN person pet ON pet.id = warning.target JOIN player plt ON plt.id = pet.primary_player - LEFT JOIN pes + LEFT JOIN person pes ON pes.id = warning.sender - LEFT JOIN pls + LEFT JOIN player pls ON pls.id = pes.primary_player WHERE warning.target = ? @@ -404,32 +409,129 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): "reason": row[9]}) return warnings +def get_warning(warn_id, acc=None, hm=None): + pe, pl = _get_ids(acc, hm) + c = conn.cursor() + sql = """SELECT + warning.id, + COALESCE(plt.account, plt.hostmask) AS target, + COALESCE(pls.account, pls.hostmask, ?) AS sender, + warning.amount, + warning.issued, + warning.expires, + CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') + THEN 0 ELSE 1 END AS expired, + warning.acknowledged, + warning.deleted, + warning.reason, + warning.notes, + COALESCE(pld.account, pld.hostmask) AS deleted_by, + warning.deleted_on + FROM warning + JOIN person pet + ON pet.id = warning.target + JOIN player plt + ON plt.id = pet.primary_player + LEFT JOIN person pes + ON pes.id = warning.sender + LEFT JOIN player pls + ON pls.id = pes.primary_player + LEFT JOIN person ped + ON ped.id = warning.deleted_by + LEFT JOIN player pld + ON pld.id = ped.primary_player + WHERE + warning.id = ? + """ + params = (botconfig.NICK, warn_id) + if acc is not None and hm is not None: + sql += """ AND warning.target = ? + AND warning.deleted = 0""" + params = (botconfig.NICK, warn_id, peid) + + c.execute(sql, params) + row = c.fetchone() + if not row: + return None + + return {"id": row[0], + "target": row[1], + "sender": row[2], + "amount": row[3], + "issued": row[4], + "expires": row[5], + "expired": row[6], + "ack": row[7], + "deleted": row[8], + "reason": row[9], + "notes": row[10], + "deleted_by": row[11], + "deleted_on": row[12], + "sanctions": get_warning_sanctions(warn_id)} + +def get_warning_sanctions(warn_id): + c = conn.cursor() + c.execute("SELECT sanction, data FROM warning_sanction WHERE warning=?", (warn_id,)) + sanctions = {} + for sanc, data in c: + if sanc == "stasis": + sanctions["stasis"] = int(data) + elif sanc == "deny command": + if "deny" not in sanctions: + sanctions["deny"] = set() + sanctions["deny"].add(data) + + return sanctions + 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) - c = conn.cursor() - c.execute("""INSERT INTO warning - ( - target, sender, amount, - issued, expires, - reasons, notes, - acknowledged - ) - VALUES - ( - ?, ?, ?, - datetime('now'), ?, - ?, ?, - ? - )""", (teid, seid, amount, expires, reasons, notes, not need_ack)) + with conn: + c = conn.cursor() + c.execute("""INSERT INTO warning + ( + target, sender, amount, + issued, expires, + reasons, notes, + acknowledged + ) + VALUES + ( + ?, ?, ?, + datetime('now'), ?, + ?, ?, + ? + )""", (teid, seid, amount, expires, reasons, notes, not need_ack)) return c.lastrowid def add_warning_sanction(warning, sanction, data): - c = conn.cursor() - c.execute("""INSERT INTO warning_sanction - (warning, sanction, data) - VALUES - (?, ?, ?)""", (warning, sanction, data)) + with conn: + c = conn.cursor() + c.execute("""INSERT INTO warning_sanction + (warning, sanction, data) + VALUES + (?, ?, ?)""", (warning, sanction, data)) + +def del_warning(warning, acc, hm): + peid, plid = _get_ids(acc, hm) + with conn: + c = conn.cursor() + c.execute("""UPDATE warning + SET + acknowledged = 1, + deleted = 1, + deleted_on = datetime('now'), + deleted_by = ? + WHERE + id = ? + AND deleted = 0""", (peid, warning)) + +def set_warning(warning, reason, notes): + with conn: + c = conn.cursor() + c.execute("""UPDATE warning + SET reason = ?, notes = ? + WHERE id = ?""", (reason, notes, warning)) def _upgrade(): # no upgrades yet, once there are some, add methods like _add_table(), _add_column(), etc. diff --git a/src/decorators.py b/src/decorators.py index 54341d6..853b286 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -10,7 +10,7 @@ from oyoyo.parse import parse_nick import botconfig import src.settings as var from src.utilities import * -from src import logger +from src import logger, db from src.messages import messages adminlog = logger("audit.log") @@ -176,8 +176,9 @@ class cmd: forced_owner_only = True break + is_owner = var.is_owner(nick, ident, host) if self.owner_only or forced_owner_only: - if var.is_owner(nick, ident, host): + if is_owner: adminlog(chan, rawnick, self.name, rest) return self.func(*largs) @@ -190,13 +191,14 @@ class cmd: # 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) - if self.flag and self.flag in flags: + 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) for command in self.cmds: - if command in denied_commands: + if command in denied_cmds: if chan == nick: pm(cli, nick, messages["invalid_permissions"]) else: @@ -204,7 +206,10 @@ class cmd: return if self.flag: - if chan == nick: + if self.flag in flags: + adminlog(chan, rawnick, self.name, rest) + return self.func(*largs) + elif chan == nick: pm(cli, nick, messages["not_an_admin"]) else: cli.notice(nick, messages["not_an_admin"]) @@ -240,3 +245,5 @@ class hook: HOOKS[each].remove(inner) if not HOOKS[each]: del HOOKS[each] + +# vim: set sw=4 expandtab: diff --git a/src/wolfgame.py b/src/wolfgame.py index 5bbf539..ccc39fc 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -8018,9 +8018,10 @@ def fwarn(cli, nick, chan, rest): # 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 - views details on warning id - # !fwarn del - deletes warning id - # !fwarn add [@] [~expiry] [sanctions] <:reason> [|notes] - # e.g. !fwarn add lykos @1 ~30d deny=goat,stats stasis=5 :Spamming|I secretly just hate him + # !fwarn del - deletes warning id + # !fwarn set [| notes] + # !fwarn add [@] [~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. # @ => warning requires acknowledgement before user can !join again @@ -8035,7 +8036,7 @@ def fwarn(cli, nick, chan, rest): # in reasons (no escaping is performed). params = re.split(" +", rest) - nick = None + target = None points = None need_ack = False expiry = None @@ -8055,8 +8056,8 @@ def fwarn(cli, nick, chan, rest): except IndexError: reply(cli, nick, chan, messages["fwarn_help_syntax"]) return - if subcommand not in ("list", "view", "add", "del", "help"): - reply(cli, nick, chan, messages["fwarn_invalid_subcommand"]) + if subcommand not in ("list", "view", "add", "del", "set", "help"): + reply(cli, nick, chan, messages["fwarn_usage"]) return reply(cli, nick, chan, messages["fwarn_{0}_syntax".format(subcommand)]) return @@ -8066,12 +8067,12 @@ def fwarn(cli, nick, chan, rest): page = 1 try: list_all = params.pop(0) - nick = params.pop(0) + target = params.pop(0) page = int(params.pop(0)) if list_all and list_all != "-all": - if nick is not None: - page = int(nick) - nick = list_all + if target is not None: + page = int(target) + target = list_all list_all = False elif show_all == "-all": list_all = True @@ -8080,14 +8081,14 @@ def fwarn(cli, nick, chan, rest): except ValueError: reply(cli, nick, chan, messages["fwarn_page_invalid"]) return - if nick is not None: - acc, hm = parse_warning_target(nick) + if target is not None: + acc, hm = parse_warning_target(target) if acc is None and hm is None: reply(cli, nick, chan, messages["fwarn_nick_invalid"]) 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(nick, points)) + reply(cli, nick, chan, messages["fwarn_list_header"].format(target, points)) else: warnings = db.list_all_warnings(list_all=list_all, skip=(page-1)*10, show=11) @@ -8098,8 +8099,8 @@ def fwarn(cli, nick, chan, rest): parts = [] if list_all: parts.append("-all") - if nick is not None: - parts.append(nick) + if target is not None: + parts.append(target) parts.append(str(page + 1)) reply(cli, nick, chan, messages["fwarn_list_footer"].format(" ".join(parts))) break @@ -8109,29 +8110,123 @@ def fwarn(cli, nick, chan, rest): expires = warn["expires"] if warn["expires"] is not None else messages["fwarn_never_expires"] if warn["deleted"]: start = "\u000314" - end = " [\u00033{0}\u000314]\u0003".format(messages["fwarn_deleted"]) + end = " [\u00034{0}\u000314]\u0003".format(messages["fwarn_deleted"]) elif warn["expired"]: start = "\u000314" - end = " [\u00037{1}\u000314]\u0003".format(messages["fwarn_expired"]) - if not warn["acknowledged"]: - ack = " \u0002!\u0002 " + end = " [\u00037{0}\u000314]\u0003".format(messages["fwarn_expired"]) + if not warn["ack"]: + ack = "\u0002!\u0002 " reply(cli, nick, chan, messages["fwarn_list"].format( start, ack, warn["id"], warn["issued"], warn["target"], - warn["sender"], warn["reason"], warn["points"], expires, end)) + warn["sender"], warn["reason"], warn["amount"], expires, end)) if i == 0: reply(cli, nick, chan, messages["fwarn_list_empty"]) return + if command == "view": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["fwarn_view_syntax"]) + return + + warning = db.get_warning(warn_id) + if warning is None: + reply(cli, nick, chan, 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"])) + + reply(cli, nick, chan, 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)) + + 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"]: + sanctions.append(messages["fwarn_view_stasis"].format(warning["sanctions"]["stasis"])) + if "deny" in warning["sanctions"]: + sanctions.append(messages["fwarn_view_deny"].format(", ".join(warning["sanctions"]["deny"]))) + if sanctions: + reply(cli, nick, chan, " ".join(sanctions)) + return + + if command == "del": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["fwarn_del_syntax"]) + return + + warning = db.get_warning(warn_id) + if warning is None: + reply(cli, nick, chan, messages["fwarn_invalid_warning"]) + return + + acc, hm = parse_warning_target(nick) + db.del_warning(warn_id, acc, hm) + reply(cli, nick, chan, messages["fwarn_done"]) + return + + if command == "set": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["fwarn_del_syntax"]) + return + + warning = db.get_warning(warn_id) + if warning is None: + reply(cli, nick, chan, messages["fwarn_invalid_warning"]) + return + + rsp = " ".join(params).split("|", 1) + if len(rsp) == 1: + rsp.append(None) + reason, notes = rsp + + reason = reason.strip() + if not reason: + reply(cli, nick, chan, messages["fwarn_reason_required"]) + return + + # 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, reason, notes) + reply(cli, nick, chan, messages["fwarn_done"]) + return + if command != "add": - reply(cli, nick, chan, messages["fwarn_invalid_subcommand"]) + reply(cli, nick, chan, messages["fwarn_usage"]) return # command == "add" while params: p = params.pop(0) - if nick is None: - # figuring out what nick actually is is handled in add_warning - nick = p + if target is None: + # figuring out what target actually is is handled in add_warning + target = p elif points is None: points = p if points[0] == "@": @@ -8148,13 +8243,10 @@ def fwarn(cli, nick, chan, rest): elif notes is not None: notes += " " + p elif reason is not None: - if p[0] == "|": - if p == "|": - notes = "" - else: - notes = p[1:] - continue - reason += " " + p + rsp = p.split("|", 1) + if len(rsp) > 1: + notes = rsp[1] + reason += " " + rsp[0] elif p[0] == ":": if p == ":": reason = "" @@ -8167,7 +8259,7 @@ def fwarn(cli, nick, chan, rest): expiry = p[1:] else: # sanctions are the only thing left here - sanc = p.split("=", 2) + sanc = p.split("=", 1) if sanc[0] == "deny": try: cmds = sanc[1].split(",") @@ -8193,15 +8285,15 @@ def fwarn(cli, nick, chan, rest): elif sanc[0] == "stasis": try: sanctions["stasis"] = int(sanc[1]) - except IndexError, ValueError: + except (IndexError, ValueError): reply(cli, nick, messages["fwarn_stasis_invalid"]) return else: reply(cli, nick, chan, messages["fwarn_sanction_invalid"]) return - if nick is None or points is None or reason is None: - cli.notice(nick, messages["fwarn_syntax"]) + if target is None or points is None or reason is None: + reply(cli, nick, chan, messages["fwarn_add_syntax"]) return reason = reason.strip() @@ -8221,6 +8313,12 @@ def fwarn(cli, nick, chan, rest): reply(cli, nick, chan, messages["fwarn_expiry_invalid_suffix"]) return + warn_id = add_warning(target, amount, nick, reason, notes, expires, need_ack, sanctions) + if warn_id is False: + reply(cli, nick, chan, messages["fwarn_cannot_add"]) + else: + reply(cli, nick, chan, messages["fwarn_added"].format(warn_id)) + @cmd("wait", "w", playing=True, phases=("join",)) def wait(cli, nick, chan, rest): """Increases the wait time until !start can be used."""