From cd3f9fc3456956a4f717a3c04d78212b6b29afa4 Mon Sep 17 00:00:00 2001 From: Ryan Schmidt Date: Tue, 2 Aug 2016 14:57:09 -0700 Subject: [PATCH] Add tempban sanction (#238) Can be either time-based or points-based. Also, made fwarn a bit smarter at guessing what the user actually wanted to do based on the parameters given to it. Warnings now always require acknowledgement, because that paves the way for a future commit only beginning stasis once a warning is acknowledged. Warnings also split off into their own file to declutter wolfgame.py a bit (now only 9k lines, wooooo! >_>) Does not play nice with eir, that functionality isn't going to be in the bot itself but rather some custom code in lykos (hooking into privmsg). --- messages/en.json | 7 +- src/db.py | 121 +++++-- src/db/db.sql | 43 ++- src/db/upgrade3.sql | 8 + src/settings.py | 9 +- src/utilities.py | 2 +- src/warnings.py | 863 ++++++++++++++++++++++++++++++++++++++++++++ src/wolfgame.py | 789 +--------------------------------------- 8 files changed, 1017 insertions(+), 825 deletions(-) create mode 100644 src/db/upgrade3.sql create mode 100644 src/warnings.py diff --git a/messages/en.json b/messages/en.json index 2629a2e..2c4ecc9 100644 --- a/messages/en.json +++ b/messages/en.json @@ -790,17 +790,18 @@ "warn_view_syntax": "Usage: warn view ", "warn_ack_syntax": "Usage: warn ack ", "warn_help_syntax": "Usage: warn help ", - "fwarn_add_syntax": "Usage: fwarn add [@] [~expiry] [sanctions] <:reason> [| notes]", + "fwarn_add_syntax": "Usage: fwarn add [~expiry] [sanctions] [:] [| 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, must be a number above 0 followed by either d, h, or m, or 'never' for a warning that never expires.", "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_sanction_invalid": "Invalid sanction, can be either deny, stasis, or tempban.", "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_tempban_invalid": "Invalid tempban amount, specify sanction as \"tempban=number\" or \"tempban=expiration\" (followed by d, h, or m).", "fwarn_list_header": "{0} has {1} active warning point{2}. Warnings prefixed with \u0002!\u0002 are unacknowledged.", "warn_list_header": "You have {0} active warning point{1}. 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} point{8}, {9}){10}", @@ -825,6 +826,7 @@ "fwarn_view_stasis_sing": "1 game of stasis.", "fwarn_view_stasis_plural": "{0} games of stasis.", "fwarn_view_deny": "denied {0}.", + "fwarn_view_tempban": "banned until {0} (if a number, indicates warning point threshold).", "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.", "no_templates": "There are no access templates defined.", @@ -846,6 +848,7 @@ "part_warning": "Parting during game. You only have a short time to re-join the channel after parting to stay alive.", "quit_warning": "Quitting IRC during game. You only have a short time to re-join the channel after quitting to stay alive.", "acc_warning": "Changing accounts during game. Please do not change accounts while playing.", + "tempban_kick": "{nick}", "_": " vim: set sw=4 expandtab:" } diff --git a/src/db.py b/src/db.py index ad5fcc8..3829619 100644 --- a/src/db.py +++ b/src/db.py @@ -6,6 +6,7 @@ import sys import time from collections import defaultdict import threading +from datetime import datetime, timedelta import botconfig import src.settings as var @@ -13,7 +14,7 @@ from src.utilities import irc_lower, break_long_message, role_order, singular # 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 = 2 +SCHEMA_VERSION = 3 _ts = threading.local() @@ -135,19 +136,37 @@ def decrement_stasis(acc=None, hostmask=None): 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 +def set_stasis(newamt, acc=None, hostmask=None, relative=False): + peid, plid = _get_ids(acc, hostmask, add=True) + _set_stasis(int(newamt), peid, relative) +def _set_stasis(newamt, peid, relative=False): conn = _conn() with conn: c = conn.cursor() - c.execute("""UPDATE person - SET stasis_amount = MIN(stasis_amount, ?) - WHERE id = ?""", (newamt, peid)) + c.execute("SELECT stasis_amount, stasis_expires FROM person WHERE id = ?", (peid,)) + oldamt, expiry = c.fetchone() + if relative: + newamt = oldamt + newamt + if newamt < 0: + newamt = 0 + if newamt > oldamt: + delta = newamt - oldamt + # increasing stasis, so need to update expiry + c.execute("""UPDATE person + SET + 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(delta)), (newamt, peid)) + else: + # decreasing stasis, don't touch expiry + c.execute("""UPDATE person + SET stasis_amount = ?, + WHERE id = ?""", (newamt, peid)) def expire_stasis(): conn = _conn() @@ -617,10 +636,9 @@ def get_warning_sanctions(warn_id): return sanctions -def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack): +def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires): teid, tlid = _get_ids(tacc, thm, add=True) seid, slid = _get_ids(sacc, shm) - ack = 0 if need_ack else 1 conn = _conn() with conn: c = conn.cursor() @@ -636,8 +654,8 @@ def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack): ?, ?, ?, datetime('now'), ?, ?, ?, - ? - )""", (teid, seid, amount, expires, reason, notes, ack)) + 0 + )""", (teid, seid, amount, expires, reason, notes)) return c.lastrowid def add_warning_sanction(warning, sanction, data): @@ -652,15 +670,28 @@ def add_warning_sanction(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)) + _set_stasis(int(data), peid, relative=True) + elif sanction == "tempban": + # we want to return a list of all banned accounts/hostmasks + idlist = set() + acclist = set() + hmlist = set() + c.execute("SELECT target FROM warning WHERE id = ?", (warning,)) + peid = c.fetchone()[0] + c.execute("SELECT id, account, hostmask FROM player WHERE person = ? AND active = 1", (peid,)) + if isinstance(data, datetime): + sql = "INSERT INTO bantrack (player, expires) values (?, ?)" + else: + sql = "INSERT INTO bantrack (player, warning_amount) values (?, ?)" + for row in c: + idlist.add(row[0]) + if row[1] is None: + hmlist.add(row[2]) + else: + acclist.add(row[1]) + for plid in idlist: + c.execute(sql, (plid, data)) + return (acclist, hmlist) def del_warning(warning, acc, hm): peid, plid = _get_ids(acc, hm) @@ -691,6 +722,46 @@ def acknowledge_warning(warning): c = conn.cursor() c.execute("UPDATE warning SET acknowledged = 1 WHERE id = ?", (warning,)) +def expire_tempbans(): + conn = _conn() + with conn: + idlist = set() + acclist = set() + hmlist = set() + c = conn.cursor() + c.execute("""SELECT + bt.player, + pl.account, + pl.hostmask + FROM bantrack bt + JOIN player pl + ON pl.id = bt.player + WHERE + (bt.expires IS NOT NULL AND bt.expires < datetime('now')) + OR ( + warning_amount IS NOT NULL + AND warning_amount <= ( + SELECT COALESCE(SUM(amount), 0) + FROM warning + WHERE + target = pl.person + AND deleted = 0 + AND ( + expires IS NULL + OR expires > datetime('now') + ) + ) + )""") + for row in c: + idlist.add(row[0]) + if row[1] is None: + hmlist.add(row[2]) + else: + acclist.add(row[1]) + for plid in idlist: + c.execute("DELETE FROM bantrack WHERE player = ?", (plid,)) + return (acclist, hmlist) + def get_pre_restart_state(): conn = _conn() with conn: @@ -741,6 +812,10 @@ def _upgrade(oldversion): # player id as a string). When nocasing players, this may cause some records to be merged. with open(os.path.join(dn, "db", "upgrade2.sql"), "rt") as f: c.executescript(f.read()) + if oldversion < 3: + print ("Upgrade from version 2 to 3...", file=sys.stderr) + with open(os.path.join(dn, "db", "upgrade3.sql"), "rt") as f: + c.executescript(f.read()) c.execute("PRAGMA user_version = " + str(SCHEMA_VERSION)) print ("Upgrades complete!", file=sys.stderr) diff --git a/src/db/db.sql b/src/db/db.sql index ebca9f9..d1ba79b 100644 --- a/src/db/db.sql +++ b/src/db/db.sql @@ -4,7 +4,7 @@ -- Player tracking. This is just what the bot decides is a unique player, two entries -- here may end up corresponding to the same actual person (see below). -CREATE TABLE IF NOT EXISTS player ( +CREATE TABLE player ( id INTEGER PRIMARY KEY, -- What person this player record belongs to person INTEGER REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED, @@ -17,13 +17,13 @@ CREATE TABLE IF NOT EXISTS player ( active BOOLEAN NOT NULL DEFAULT 1 ); -CREATE INDEX IF NOT EXISTS player_idx ON player (account, hostmask, active); -CREATE INDEX IF NOT EXISTS person_idx ON player (person); +CREATE INDEX player_idx ON player (account, hostmask, active); +CREATE INDEX person_idx ON player (person); -- Person tracking; a person can consist of multiple players (for example, someone may have -- an account player for when they are logged in and 3 hostmask players for when they are -- logged out depending on what connection they are using). -CREATE TABLE IF NOT EXISTS person ( +CREATE TABLE person ( id INTEGER PRIMARY KEY, -- Primary player for this person primary_player INTEGER NOT NULL UNIQUE REFERENCES player(id) DEFERRABLE INITIALLY DEFERRED, @@ -45,7 +45,7 @@ CREATE TABLE IF NOT EXISTS person ( -- Sometimes people are bad, this keeps track of that for the purpose of automatically applying -- various sanctions and viewing the past history of someone. Outside of specifically-marked -- fields, records are never modified or deleted from this table once inserted. -CREATE TABLE IF NOT EXISTS warning ( +CREATE TABLE warning ( id INTEGER PRIMARY KEY, -- The target (recipient) of the warning target INTEGER NOT NULL REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED, @@ -73,13 +73,13 @@ CREATE TABLE IF NOT EXISTS warning ( deleted_on DATETIME ); -CREATE INDEX IF NOT EXISTS warning_idx ON warning (target, deleted, issued); -CREATE INDEX IF NOT EXISTS warning_sender_idx ON warning (target, sender, deleted, issued); +CREATE INDEX warning_idx ON warning (target, deleted, issued); +CREATE INDEX warning_sender_idx ON warning (target, sender, deleted, issued); -- In addition to giving warning points, a warning may have specific sanctions attached -- that apply until the warning expires; for example preventing a user from joining deadchat -- or denying them access to a particular command (such as !goat). -CREATE TABLE IF NOT EXISTS warning_sanction ( +CREATE TABLE warning_sanction ( -- The warning this sanction is attached to warning INTEGER NOT NULL REFERENCES warning(id) DEFERRABLE INITIALLY DEFERRED, -- The type of sanction this is @@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS warning_sanction ( -- This shouldn't be too horribly slow, but if it is some strategies can be employed to speed it up: -- On startup, aggregate everything from this table and store in-memory, then increment those in-memory -- counts as games are played. -CREATE TABLE IF NOT EXISTS game ( +CREATE TABLE game ( id INTEGER PRIMARY KEY, -- The gamemode played gamemode TEXT NOT NULL COLLATE NOCASE, @@ -110,10 +110,10 @@ CREATE TABLE IF NOT EXISTS game ( winner TEXT COLLATE NOCASE ); -CREATE INDEX IF NOT EXISTS game_idx ON game (gamemode, gamesize); +CREATE INDEX game_idx ON game (gamemode, gamesize); -- List of people who played in each game -CREATE TABLE IF NOT EXISTS game_player ( +CREATE TABLE game_player ( id INTEGER PRIMARY KEY, game INTEGER NOT NULL REFERENCES game(id) DEFERRABLE INITIALLY DEFERRED, player INTEGER NOT NULL REFERENCES player(id) DEFERRABLE INITIALLY DEFERRED, @@ -125,11 +125,11 @@ CREATE TABLE IF NOT EXISTS game_player ( dced BOOLEAN NOT NULL ); -CREATE INDEX IF NOT EXISTS game_player_game_idx ON game_player (game); -CREATE INDEX IF NOT EXISTS game_player_player_idx ON game_player (player); +CREATE INDEX game_player_game_idx ON game_player (game); +CREATE INDEX game_player_player_idx ON game_player (player); -- List of all roles and other special qualities (e.g. lover, entranced, etc.) the player had in game -CREATE TABLE IF NOT EXISTS game_player_role ( +CREATE TABLE game_player_role ( game_player INTEGER NOT NULL REFERENCES game_player(id) DEFERRABLE INITIALLY DEFERRED, -- Name of the role or other quality recorded role TEXT NOT NULL COLLATE NOCASE, @@ -137,11 +137,11 @@ CREATE TABLE IF NOT EXISTS game_player_role ( special BOOLEAN NOT NULL ); -CREATE INDEX IF NOT EXISTS game_player_role_idx ON game_player_role (game_player); +CREATE INDEX game_player_role_idx ON game_player_role (game_player); -- Access templates; instead of manually specifying flags, a template can be used to add a group of -- flags simultaneously. -CREATE TABLE IF NOT EXISTS access_template ( +CREATE TABLE access_template ( id INTEGER PRIMARY KEY, -- Template name, for display purposes name TEXT NOT NULL, @@ -150,7 +150,7 @@ CREATE TABLE IF NOT EXISTS access_template ( ); -- Access control, owners still need to be specified in botconfig, but everyone else goes here -CREATE TABLE IF NOT EXISTS access ( +CREATE TABLE access ( person INTEGER NOT NULL PRIMARY KEY REFERENCES person(id) DEFERRABLE INITIALLY DEFERRED, -- Template to base this person's access on, or NULL if it is not based on a template template INTEGER REFERENCES access_template(id) DEFERRABLE INITIALLY DEFERRED, @@ -159,8 +159,15 @@ CREATE TABLE IF NOT EXISTS access ( flags TEXT ); +-- Holds bans that the bot is tracking (due to sanctions) +CREATE TABLE bantrack ( + player INTEGER NOT NULL PRIMARY KEY REFERENCES player(id) DEFERRABLE INITIALLY DEFERRED, + expires DATETIME, + warning_amount INTEGER +); + -- Used to hold state between restarts -CREATE TABLE IF NOT EXISTS pre_restart_state ( +CREATE TABLE pre_restart_state ( -- List of players to ping after the bot comes back online players TEXT ); diff --git a/src/db/upgrade3.sql b/src/db/upgrade3.sql new file mode 100644 index 0000000..9a51c2e --- /dev/null +++ b/src/db/upgrade3.sql @@ -0,0 +1,8 @@ +-- upgrade script to migrate from version 2 to version 3 + +CREATE TABLE bantrack ( + player INTEGER NOT NULL PRIMARY KEY REFERENCES player(id) DEFERRABLE INITIALLY DEFERRED, + expires DATETIME, + warning_amount INTEGER +); + diff --git a/src/settings.py b/src/settings.py index d377517..66b4910 100644 --- a/src/settings.py +++ b/src/settings.py @@ -57,6 +57,7 @@ DEVOICE_DURING_NIGHT = False ALWAYS_PM_ROLE = False QUIET_MODE = "q" # "q" or "b" QUIET_PREFIX = "" # "" or "~q:" +ACCOUNT_PREFIX = "$a:" # "$a:" or "~a:" # The bot will automatically toggle those modes of people joining AUTO_TOGGLE_MODES = "" @@ -70,6 +71,9 @@ PART_EXPIRY = "30d" ACC_PENALTY = 1 ACC_EXPIRY = "30d" +# If True, disallows adding stasis via !fstasis (requires warnings instead) +RESTRICT_FSTASIS = True + # The formatting of this sucks, sorry. This is used to automatically apply sanctions to warning levels # When a user crosses from below the min threshold to min or above points, the listed sanctions apply # Sanctions also apply while moving within the same threshold bracket (such as from min to max) @@ -77,13 +81,10 @@ ACC_EXPIRY = "30d" # Scalestasis applies stasis equal to the formula ax^2 + bx + c, where x is the number of warning points # Tempban number can either be a duration (ending in d, h, or m) or a number meaning it expires when # warning points fall below that threshold. -# Tempban is currently not implemented and does nothing right now. AUTO_SANCTION = ( #min max sanctions - (1, 4, {"ack": True}), (5, 9, {"stasis": 1}), - (10, 10, {"ack": True, "stasis": 3}), - (11, 14, {"stasis": 3}), + (10, 14, {"stasis": 3}), (15, 24, {"scalestasis": (0, 1, -10)}), (25, 25, {"tempban": 15}) ) diff --git a/src/utilities.py b/src/utilities.py index e8d93f8..849fba8 100644 --- a/src/utilities.py +++ b/src/utilities.py @@ -34,7 +34,7 @@ def mass_mode(cli, md_param, md_plain): arg1 = "".join(md_plain) + "".join(z[0]) arg2 = " ".join(z[1]) # + " " + " ".join([x+"!*@*" for x in z[1]]) cli.mode(botconfig.CHANNEL, arg1, arg2) - else: + elif md_plain: cli.mode(botconfig.CHANNEL, "".join(md_plain)) def mass_privmsg(cli, targets, msg, notice=False, privmsg=False): diff --git a/src/warnings.py b/src/warnings.py new file mode 100644 index 0000000..3e8c4e2 --- /dev/null +++ b/src/warnings.py @@ -0,0 +1,863 @@ +from datetime import datetime, timedelta + +import botconfig +import src.settings as var +from src import db +from src.utilities import * +from src.decorators import cmd +from src.events import Event +from src.messages import messages + +__all__ = ["is_user_stasised", "decrement_stasis", "parse_warning_target", "add_warning", "expire_tempbans"] + +def is_user_stasised(nick): + """Checks if a user is in stasis. Returns a number of games in stasis.""" + + if nick in var.USERS: + ident = irc_lower(var.USERS[nick]["ident"]) + host = var.USERS[nick]["host"].lower() + acc = irc_lower(var.USERS[nick]["account"]) + else: + return -1 + amount = 0 + if not var.DISABLE_ACCOUNTS and acc and acc != "*": + if acc in var.STASISED_ACCS: + amount = var.STASISED_ACCS[acc] + for hostmask in var.STASISED: + if match_hostmask(hostmask, nick, ident, host): + amount = max(amount, var.STASISED[hostmask]) + return amount + +def decrement_stasis(nick=None): + if nick and nick in var.USERS: + ident = irc_lower(var.USERS[nick]["ident"]) + host = var.USERS[nick]["host"].lower() + acc = irc_lower(var.USERS[nick]["account"]) + # decrement account stasis even if accounts are disabled + if acc in var.STASISED_ACCS: + db.decrement_stasis(acc=acc) + for hostmask in var.STASISED: + if match_hostmask(hostmask, nick, ident, host): + db.decrement_stasis(hostmask=hostmask) + else: + db.decrement_stasis() + # Also expire any expired stasis and tempbans and update our tracking vars + db.expire_stasis() + db.init_vars() + +def expire_tempbans(cli): + acclist, hmlist = db.expire_tempbans() + cmodes = [] + for acc in acclist: + cmodes.append(("-b", "{0}{1}".format(var.ACCOUNT_PREFIX, acc))) + for hm in hmlist: + cmodes.append(("-b", "*!*@{0}".format(hm))) + mass_mode(cli, cmodes, []) + +def parse_warning_target(target, lower=False): + if target[0] == "=": + if var.DISABLE_ACCOUNTS: + return (None, None) + tacc = target[1:] + thm = None + if lower: + tacc = irc_lower(tacc) + elif target in var.USERS: + tacc = var.USERS[target]["account"] + ident = var.USERS[target]["ident"] + host = var.USERS[target]["host"] + if lower: + tacc = irc_lower(tacc) + ident = irc_lower(ident) + host = host.lower() + thm = target + "!" + ident + "@" + host + elif "@" in target: + tacc = None + thm = target + if lower: + hml, hmr = thm.split("@", 1) + thm = irc_lower(hml) + "@" + hmr.lower() + elif not var.DISABLE_ACCOUNTS: + tacc = target + thm = None + if lower: + tacc = irc_lower(tacc) + else: + return (None, None) + return (tacc, thm) + +def add_warning(cli, target, amount, actor, reason, notes=None, expires=None, sanctions=None): + # make 0-point warnings no-op successfully, otherwise we add warnings when things like PART_PENALTY is 0 + if amount == 0: + return False + + tacc, thm = parse_warning_target(target) + if tacc is None and thm is None: + return False + + if actor not in var.USERS and actor != botconfig.NICK: + return False + sacc = None + shm = None + if actor in var.USERS: + sacc = var.USERS[actor]["account"] + shm = actor + "!" + var.USERS[actor]["ident"] + "@" + var.USERS[actor]["host"] + + # Turn expires into a datetime if we were passed a string; note that no error checking is performed here + if isinstance(expires, str): + exp_suffix = expires[-1] + exp_amount = int(expires[:-1]) + + if exp_suffix == "d": + expires = datetime.utcnow() + timedelta(days=exp_amount) + elif exp_suffix == "h": + expires = datetime.utcnow() + timedelta(hours=exp_amount) + elif exp_suffix == "m": + expires = datetime.utcnow() + timedelta(minutes=exp_amount) + else: + raise ValueError("Invalid expiration string") + elif isinstance(expires, int): + expires = datetime.utcnow() + timedelta(days=expires) + + # Round expires to the nearest minute (30s rounds up) + if isinstance(expires, datetime): + round_add = 0 + if expires.second >= 30: + round_add = 1 + expires -= timedelta(seconds=expires.second, microseconds=expires.microsecond) + expires += timedelta(minutes=round_add) + + # determine if we need to automatically add any sanctions + if sanctions is None: + sanctions = {} + prev = db.get_warning_points(tacc, thm) + cur = prev + amount + for (mn, mx, sanc) in var.AUTO_SANCTION: + if (prev < mn and cur >= mn) or (prev >= mn and prev <= mx and cur <= mx): + if "stasis" in sanc: + if "stasis" not in sanctions: + sanctions["stasis"] = sanc["stasis"] + else: + sanctions["stasis"] = max(sanctions["stasis"], sanc["stasis"]) + if "scalestasis" in sanc: + (a, b, c) = sanc["scalestasis"] + amt = (a * cur * cur) + (b * cur) + c + if "stasis" not in sanctions: + sanctions["stasis"] = amt + else: + sanctions["stasis"] = max(sanctions["stasis"], amt) + if "deny" in sanc: + if "deny" not in sanctions: + sanctions["deny"] = set(sanc["deny"]) + else: + sanctions["deny"].update(sanc["deny"]) + if "tempban" in sanc: + # tempban's param can either be a fixed expiry time or a number + # which indicates the warning point threshold that the ban will be lifted at + # if two are set at once, the threshold takes precedence over set times + # within each category, a larger set time or a lower threshold takes precedence + exp = None + ths = None + if sanc["tempban"][-1] in ("d", "h", "m"): + amt = int(sanc["tempban"][:-1]) + dur = sanc["tempban"][-1] + if dur == "d": + exp = datetime.utcnow() + timedelta(days=amt) + elif dur == "h": + exp = datetime.utcnow() + timedelta(hours=amt) + elif dur == "m": + exp = datetime.utcnow() + timedelta(minutes=amt) + else: + ths = int(sanc["tempban"]) + + if "tempban" in sanctions: + if isinstance(sanctions["tempban"], datetime): + if ths is not None: + sanctions["tempban"] = ths + else: + sanctions["tempban"] = max(sanctions["tempban"], exp) + elif ths is not None: + sanctions["tempban"] = min(sanctions["tempban"], ths) + elif ths is not None: + sanctions["tempban"] = ths + else: + sanctions["tempban"] = exp + + sid = db.add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires) + if "stasis" in sanctions: + db.add_warning_sanction(sid, "stasis", sanctions["stasis"]) + if "deny" in sanctions: + for cmd in sanctions["deny"]: + db.add_warning_sanction(sid, "deny command", cmd) + if "tempban" in sanctions: + # this inserts into the bantrack table too + (acclist, hmlist) = db.add_warning_sanction(sid, "tempban", sanctions["tempban"]) + cmodes = [] + for acc in acclist: + cmodes.append(("+b", "{0}{1}".format(var.ACCOUNT_PREFIX, acc))) + for hm in hmlist: + cmodes.append(("+b", "*!*@{0}".format(hm))) + mass_mode(cli, cmodes, []) + for (nick, user) in var.USERS.items(): + if user["account"] in acclist: + cli.kick(botconfig.CHANNEL, nick, messages["tempban_kick"].format(nick=nick, botnick=botconfig.NICK, reason=reason)) + elif user["host"] in hmlist: + cli.kick(botconfig.CHANNEL, nick, messages["tempban_kick"].format(nick=nick, botnick=botconfig.NICK, reason=reason)) + + # Update any tracking vars that may have changed due to this + db.init_vars() + + return sid + +@cmd("stasis", chan=True, pm=True) +def stasis(cli, nick, chan, rest): + st = is_user_stasised(nick) + if st: + msg = messages["your_current_stasis"].format(st, "" if st == 1 else "s") + else: + msg = messages["you_not_in_stasis"] + + 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], lower=True) + 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 and var.RESTRICT_FSTASIS: + 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.set_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] [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. + 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) + reply(cli, nick, chan, messages["warn_list_header"].format(points, "" if points == 1 else "s"), private=True) + + i = 0 + for warn in warnings: + i += 1 + if (i == 11): + parts = [] + if list_all: + parts.append("-all") + parts.append(str(page + 1)) + reply(cli, nick, chan, messages["warn_list_footer"].format(" ".join(parts)), private=True) + 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 " + reply(cli, nick, chan, messages["warn_list"].format( + start, ack, warn["id"], warn["issued"], warn["reason"], warn["amount"], + "" if warn["amount"] == 1 else "s", expires, end), private=True) + if i == 0: + reply(cli, nick, chan, messages["fwarn_list_empty"], private=True) + 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): + 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"])) + + reply(cli, nick, chan, messages["warn_view_header"].format( + warning["id"], warning["issued"], warning["amount"], + "" if warning["amount"] == 1 else "s", expires), private=True) + reply(cli, nick, chan, warning["reason"], private=True) + + 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: + reply(cli, nick, chan, " ".join(sanctions), private=True) + return + + if command == "ack": + try: + warn_id = params.pop(0) + if warn_id[0] == "#": + warn_id = warn_id[1:] + warn_id = int(warn_id) + 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="F", pm=True) +def fwarn(cli, nick, chan, rest): + """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 - views details on warning id + # !fwarn del - deletes warning id + # !fwarn set [~expiry] [reason] [| notes] + # !fwarn add [~expiry] [sanctions] [:] [| 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 = re.split(" +", rest) + target = None + points = None + expires = None + sanctions = {} + reason = None + notes = None + + try: + command = params.pop(0) + except IndexError: + reply(cli, nick, chan, 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: + reply(cli, nick, chan, messages["fwarn_help_syntax"]) + return + 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 + + 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": + 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: + reply(cli, nick, chan, 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) + reply(cli, nick, chan, messages["fwarn_list_header"].format(target, points, "" if points == 1 else "s"), private=True) + 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)) + reply(cli, nick, chan, messages["fwarn_list_footer"].format(" ".join(parts)), private=True) + 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 " + reply(cli, nick, chan, 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), private=True) + if i == 0: + reply(cli, nick, chan, messages["fwarn_list_empty"], private=True) + 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): + 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"], "" if warning["amount"] == 1 else "s", expires), private=True) + + reason = [warning["reason"]] + if warning["notes"] is not None: + reason.append(warning["notes"]) + reply(cli, nick, chan, " | ".join(reason), private=True) + + 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: + reply(cli, nick, chan, " ".join(sanctions), private=True) + 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): + 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 = params.pop(0) + if warn_id[0] == "#": + warn_id = warn_id[1:] + warn_id = int(warn_id) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["fwarn_set_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() + + # 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: + reply(cli, nick, chan, messages["fwarn_expiry_invalid"]) + return + + if amount <= 0: + reply(cli, nick, chan, 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: + reply(cli, nick, chan, 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) + reply(cli, nick, chan, messages["fwarn_done"]) + 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: + reply(cli, nick, chan, messages["fwarn_points_invalid"]) + return + if points < 1: + reply(cli, nick, chan, 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 == "~": + reply(cli, nick, chan, 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: + reply(cli, nick, chan, messages["fwarn_deny_invalid_command"].format(cmd)) + return + normalized_cmds.add(normalized) + sanctions["deny"] = normalized_cmds + except IndexError: + reply(cli, nick, chan, messages["fwarn_deny_invalid"]) + return + elif sanc[0] == "stasis": + try: + sanctions["stasis"] = int(sanc[1]) + except (IndexError, ValueError): + reply(cli, nick, chan, 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): + reply(cli, nick, chan, 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: + reply(cli, nick, chan, 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(cli, target, points, nick, reason, notes, expires, sanctions) + except ValueError: + reply(cli, nick, chan, messages["fwarn_expiry_invalid"]) + + if warn_id is False: + reply(cli, nick, chan, messages["fwarn_cannot_add"]) + else: + reply(cli, nick, chan, messages["fwarn_added"].format(warn_id)) + + +# vim: set sw=4 expandtab: diff --git a/src/wolfgame.py b/src/wolfgame.py index fc3ab65..6f21767 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -29,7 +29,6 @@ import random import re import signal import socket -import sqlite3 import string import subprocess import sys @@ -47,6 +46,7 @@ import src.settings as var from src.utilities import * from src import db, decorators, events, logger, proxy, debuglog, errlog, plog from src.messages import messages +from src.warnings import * # done this way so that events is accessible in !eval (useful for debugging) Event = events.Event @@ -215,6 +215,9 @@ def connect_callback(cli): for nick in to_be_devoiced: cmodes.append(("-v", nick)) + # Expire tempbans + expire_tempbans(cli) + # If the bot was restarted in the middle of the join phase, ping players that were joined. players = db.get_pre_restart_state() if players: @@ -1268,6 +1271,7 @@ def kill_join(cli, chan): # use this opportunity to expire pending stasis db.expire_stasis() db.init_vars() + expire_tempbans(cli) if var.AFTER_FLASTGAME is not None: var.AFTER_FLASTGAME() var.AFTER_FLASTGAME = None @@ -1364,7 +1368,7 @@ def fleave(cli, nick, chan, rest): if a in rset: var.ORIGINAL_ROLES[r].remove(a) var.ORIGINAL_ROLES[r].add("(dced)"+a) - add_warning(a, var.LEAVE_PENALTY, botconfig.NICK, messages["leave_warning"], expires=var.LEAVE_EXPIRY) + add_warning(cli, a, var.LEAVE_PENALTY, botconfig.NICK, messages["leave_warning"], expires=var.LEAVE_EXPIRY) if a in var.PLAYERS: var.DCED_PLAYERS[a] = var.PLAYERS.pop(a) @@ -2681,8 +2685,8 @@ def stop_game(cli, winner="", abort=False, additional_winners=None, log=True): mass_privmsg(cli, var.DEADCHAT_PLAYERS, messages["endgame_deadchat"].format(chan)) reset_modes_timers(cli) - reset() + expire_tempbans(cli) # This must be after reset() if var.AFTER_FLASTGAME is not None: @@ -3366,7 +3370,7 @@ def reaper(cli, gameid): if nck in rlist: var.ORIGINAL_ROLES[r].remove(nck) var.ORIGINAL_ROLES[r].add("(dced)"+nck) - add_warning(nck, var.IDLE_PENALTY, botconfig.NICK, messages["idle_warning"], expires=var.IDLE_EXPIRY) + add_warning(cli, nck, var.IDLE_PENALTY, botconfig.NICK, messages["idle_warning"], expires=var.IDLE_EXPIRY) del_player(cli, nck, end_game = False, death_triggers = False) chk_win(cli) pl = list_players() @@ -3383,7 +3387,7 @@ def reaper(cli, gameid): else: cli.msg(chan, messages["quit_death_no_reveal"].format(dcedplayer)) if var.PHASE != "join": - add_warning(dcedplayer, var.PART_PENALTY, botconfig.NICK, messages["quit_warning"], expires=var.PART_EXPIRY) + add_warning(cli, dcedplayer, var.PART_PENALTY, botconfig.NICK, messages["quit_warning"], expires=var.PART_EXPIRY) if not del_player(cli, dcedplayer, devoice = False, death_triggers = False): return elif what == "part" and (datetime.now() - timeofdc) > timedelta(seconds=var.PART_GRACE_TIME): @@ -3392,7 +3396,7 @@ def reaper(cli, gameid): else: cli.msg(chan, messages["part_death_no_reveal"].format(dcedplayer)) if var.PHASE != "join": - add_warning(dcedplayer, var.PART_PENALTY, botconfig.NICK, messages["part_warning"], expires=var.PART_EXPIRY) + add_warning(cli, dcedplayer, var.PART_PENALTY, botconfig.NICK, messages["part_warning"], expires=var.PART_EXPIRY) if not del_player(cli, dcedplayer, devoice = False, death_triggers = False): return elif what == "account" and (datetime.now() - timeofdc) > timedelta(seconds=var.ACC_GRACE_TIME): @@ -3401,7 +3405,7 @@ def reaper(cli, gameid): else: cli.msg(chan, messages["account_death_no_reveal"].format(dcedplayer)) if var.PHASE != "join": - add_warning(dcedplayer, var.ACC_PENALTY, botconfig.NICK, messages["acc_warning"], expires=var.ACC_EXPIRY) + add_warning(cli, dcedplayer, var.ACC_PENALTY, botconfig.NICK, messages["acc_warning"], expires=var.ACC_EXPIRY) if not del_player(cli, dcedplayer, devoice = False, death_triggers = False): return time.sleep(10) @@ -3884,7 +3888,7 @@ def leave_game(cli, nick, chan, rest): if nick in rset: var.ORIGINAL_ROLES[r].remove(nick) var.ORIGINAL_ROLES[r].add("(dced)"+nick) - add_warning(nick, var.LEAVE_PENALTY, botconfig.NICK, messages["leave_warning"], expires=var.LEAVE_EXPIRY) + add_warning(cli, nick, var.LEAVE_PENALTY, botconfig.NICK, messages["leave_warning"], expires=var.LEAVE_EXPIRY) if nick in var.PLAYERS: var.DCED_PLAYERS[nick] = var.PLAYERS.pop(nick) @@ -7779,775 +7783,6 @@ def on_error(cli, pfx, msg): elif msg.startswith("Closing Link:"): raise SystemExit -def is_user_stasised(nick): - """Checks if a user is in stasis. Returns a number of games in stasis.""" - - if nick in var.USERS: - ident = irc_lower(var.USERS[nick]["ident"]) - host = var.USERS[nick]["host"].lower() - acc = irc_lower(var.USERS[nick]["account"]) - else: - return -1 - amount = 0 - if not var.DISABLE_ACCOUNTS and acc and acc != "*": - if acc in var.STASISED_ACCS: - amount = var.STASISED_ACCS[acc] - for hostmask in var.STASISED: - if match_hostmask(hostmask, nick, ident, host): - amount = max(amount, var.STASISED[hostmask]) - return amount - -def decrement_stasis(nick=None): - if nick and nick in var.USERS: - ident = irc_lower(var.USERS[nick]["ident"]) - host = var.USERS[nick]["host"].lower() - acc = irc_lower(var.USERS[nick]["account"]) - # decrement account stasis even if accounts are disabled - if acc in var.STASISED_ACCS: - db.decrement_stasis(acc=acc) - for hostmask in var.STASISED: - if match_hostmask(hostmask, nick, ident, host): - db.decrement_stasis(hostmask=hostmask) - else: - db.decrement_stasis() - # Also expire any expired stasis and update our tracking vars - db.expire_stasis() - db.init_vars() - -def parse_warning_target(target, lower=False): - if target[0] == "=": - if var.DISABLE_ACCOUNTS: - return (None, None) - tacc = target[1:] - thm = None - if lower: - tacc = irc_lower(tacc) - elif target in var.USERS: - tacc = var.USERS[target]["account"] - ident = var.USERS[target]["ident"] - host = var.USERS[target]["host"] - if lower: - tacc = irc_lower(tacc) - ident = irc_lower(ident) - host = host.lower() - thm = target + "!" + ident + "@" + host - elif "@" in target: - tacc = None - thm = target - if lower: - hml, hmr = thm.split("@", 1) - thm = irc_lower(hml) + "@" + hmr.lower() - elif not var.DISABLE_ACCOUNTS: - tacc = target - thm = None - if lower: - tacc = irc_lower(tacc) - else: - return (None, None) - return (tacc, thm) - -def add_warning(target, amount, actor, reason, notes=None, expires=None, need_ack=False, sanctions=None): - # make 0-point warnings no-op successfully, otherwise we add warnings when things like PART_PENALTY is 0 - if amount == 0: - return False - - tacc, thm = parse_warning_target(target) - if tacc is None and thm is None: - return False - - if actor not in var.USERS and actor != botconfig.NICK: - return False - sacc = None - shm = None - if actor in var.USERS: - sacc = var.USERS[actor]["account"] - shm = actor + "!" + var.USERS[actor]["ident"] + "@" + var.USERS[actor]["host"] - - # Turn expires into a datetime if we were passed a string; note that no error checking is performed here - if isinstance(expires, str): - exp_suffix = expires[-1] - exp_amount = int(expires[:-1]) - - if exp_suffix == "d": - expires = datetime.utcnow() + timedelta(days=exp_amount) - elif exp_suffix == "h": - expires = datetime.utcnow() + timedelta(hours=exp_amount) - elif exp_suffix == "m": - expires = datetime.utcnow() + timedelta(minutes=exp_amount) - else: - raise ValueError("Invalid expiration string") - elif isinstance(expires, int): - expires = datetime.utcnow() + timedelta(days=expires) - - # Round expires to the nearest minute (30s rounds up) - if isinstance(expires, datetime): - round_add = 0 - if expires.second >= 30: - round_add = 1 - expires -= timedelta(seconds=expires.second, microseconds=expires.microsecond) - expires += timedelta(minutes=round_add) - - # determine if we need to automatically add any sanctions - if sanctions is None: - sanctions = {} - prev = db.get_warning_points(tacc, thm) - cur = prev + amount - for (mn, mx, sanc) in var.AUTO_SANCTION: - if (prev < mn and cur >= mn) or (prev >= mn and prev <= mx and cur <= mx): - if "ack" in sanc: - need_ack = True - if "stasis" in sanc: - if "stasis" not in sanctions: - sanctions["stasis"] = sanc["stasis"] - else: - sanctions["stasis"] = max(sanctions["stasis"], sanc["stasis"]) - if "scalestasis" in sanc: - (a, b, c) = sanc["scalestasis"] - amt = (a * cur * cur) + (b * cur) + c - if "stasis" not in sanctions: - sanctions["stasis"] = amt - else: - sanctions["stasis"] = max(sanctions["stasis"], amt) - if "deny" in sanc: - if "deny" not in sanctions: - sanctions["deny"] = set(sanc["deny"]) - else: - sanctions["deny"].update(sanc["deny"]) - if "tempban" in sanc: - # XXX: need to do this somehow, leaving here as a reminder for later - pass - - sid = db.add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack) - if "stasis" in sanctions: - db.add_warning_sanction(sid, "stasis", sanctions["stasis"]) - if "deny" in sanctions: - 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) -def stasis(cli, nick, chan, rest): - st = is_user_stasised(nick) - if st: - msg = messages["your_current_stasis"].format(st, "" if st == 1 else "s") - else: - msg = messages["you_not_in_stasis"] - - 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], lower=True) - 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] [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. - 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) - reply(cli, nick, chan, messages["warn_list_header"].format(points, "" if points == 1 else "s"), private=True) - - i = 0 - for warn in warnings: - i += 1 - if (i == 11): - parts = [] - if list_all: - parts.append("-all") - parts.append(str(page + 1)) - reply(cli, nick, chan, messages["warn_list_footer"].format(" ".join(parts)), private=True) - 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 " - reply(cli, nick, chan, messages["warn_list"].format( - start, ack, warn["id"], warn["issued"], warn["reason"], warn["amount"], - "" if warn["amount"] == 1 else "s", expires, end), private=True) - if i == 0: - reply(cli, nick, chan, messages["fwarn_list_empty"], private=True) - 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): - 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"])) - - reply(cli, nick, chan, messages["warn_view_header"].format( - warning["id"], warning["issued"], warning["amount"], - "" if warning["amount"] == 1 else "s", expires), private=True) - reply(cli, nick, chan, warning["reason"], private=True) - - 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: - reply(cli, nick, chan, " ".join(sanctions), private=True) - return - - if command == "ack": - try: - warn_id = params.pop(0) - if warn_id[0] == "#": - warn_id = warn_id[1:] - warn_id = int(warn_id) - 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="F", pm=True) -def fwarn(cli, nick, chan, rest): - """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 - views details on warning id - # !fwarn del - deletes warning id - # !fwarn set [~expiry] [reason] [| 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 - # 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. Must be prefixed 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 = re.split(" +", rest) - target = None - points = None - need_ack = False - expires = None - sanctions = {} - reason = None - notes = None - - try: - command = params.pop(0) - except IndexError: - 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) - except IndexError: - reply(cli, nick, chan, messages["fwarn_help_syntax"]) - return - 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 - - 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": - 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: - reply(cli, nick, chan, 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) - reply(cli, nick, chan, messages["fwarn_list_header"].format(target, points, "" if points == 1 else "s"), private=True) - 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)) - reply(cli, nick, chan, messages["fwarn_list_footer"].format(" ".join(parts)), private=True) - 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 " - reply(cli, nick, chan, 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), private=True) - if i == 0: - reply(cli, nick, chan, messages["fwarn_list_empty"], private=True) - 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): - 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"], "" if warning["amount"] == 1 else "s", expires), private=True) - - reason = [warning["reason"]] - if warning["notes"] is not None: - reason.append(warning["notes"]) - reply(cli, nick, chan, " | ".join(reason), private=True) - - 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 sanctions: - reply(cli, nick, chan, " ".join(sanctions), private=True) - 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): - 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 = params.pop(0) - if warn_id[0] == "#": - warn_id = warn_id[1:] - warn_id = int(warn_id) - except (IndexError, ValueError): - reply(cli, nick, chan, messages["fwarn_set_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() - - # 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: - reply(cli, nick, chan, messages["fwarn_expiry_invalid"]) - return - - if amount <= 0: - reply(cli, nick, chan, 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: - reply(cli, nick, chan, 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) - reply(cli, nick, chan, messages["fwarn_done"]) - 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: - points = p - if points[0] == "@": - points = points[1:] - need_ack = True - try: - points = int(points) - except ValueError: - reply(cli, nick, chan, messages["fwarn_points_invalid"]) - return - if points < 1: - reply(cli, nick, chan, 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 == "~": - reply(cli, nick, chan, 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: - reply(cli, nick, chan, messages["fwarn_deny_invalid_command"].format(cmd)) - return - normalized_cmds.add(normalized) - sanctions["deny"] = normalized_cmds - except IndexError: - reply(cli, nick, chan, messages["fwarn_deny_invalid"]) - return - elif sanc[0] == "stasis": - try: - sanctions["stasis"] = int(sanc[1]) - except (IndexError, ValueError): - reply(cli, nick, chan, messages["fwarn_stasis_invalid"]) - return - else: - reply(cli, nick, chan, messages["fwarn_sanction_invalid"]) - return - - if target is None or points is None or reason is None: - reply(cli, nick, chan, 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(target, points, nick, reason, notes, expires, need_ack, sanctions) - except ValueError: - reply(cli, nick, chan, messages["fwarn_expiry_invalid"]) - - 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("ftemplate", flag="F", pm=True) def ftemplate(cli, nick, chan, rest): params = re.split(" +", rest)