Finish warning system

- fwarn/warn commands to view and manipulate warnings
- fstasis can now only decrease stasis, not add to it
- refreshdb command can sync bot game state with what is in the db
  (including expiring any unexpired stasis or warnings)
- stasis now expires
- tempban is still not implemented and will not be implemented as
  part of the PR (it will come later, if ever)
- sanctions can be automatically applied after warnings cross a
  certain threshold; some defaults are configured
This commit is contained in:
skizzerz 2016-06-07 14:17:21 -05:00
parent bba5ab745e
commit a6ea55a8fe
6 changed files with 492 additions and 138 deletions

View File

@ -574,6 +574,7 @@
"account_not_in_stasis": "\u0002{0}\u0002 (Account: {1}) is not in stasis.",
"currently_stasised": "Currently stasised: {0}",
"noone_stasised": "Nobody is currently stasised.",
"stasis_cannot_increase": "Cannot increase stasis using fstasis; use fwarn instead.",
"no_command_specified": "Error: No command specified. Did you mean \u0002-cmds\u0002?",
"invalid_option": "Invalid option: {0}",
"command_does_not_exist": "That command does not exist.",
@ -797,14 +798,20 @@
"villagergame_nope": "Game over! The villagers decided incorrectly that there are actually no wolves, allowing the wolves to slaughter the remainder of them in their sleep with impunity.",
"stop_bot_ingame_safeguard": "Warning: A game is currently running. If you want to {what} the bot anyway, use \"{prefix}{cmd} -force\".",
"fwarn_usage": "Usage: fwarn list|view|add|del|set|help. See fwarn help <subcommand> for more details.",
"warn_usage": "Usage: warn list|view|ack|help. See warn help <subcommand> for more details.",
"fwarn_list_syntax": "Usage: fwarn list [-all] [nick[!user@host]|=account] [page]",
"fwarn_view_syntax": "Usage: fwarn view <id>",
"fwarn_del_syntax": "Usage: fwarn del <id>",
"fwarn_set_syntax": "Usage: fwarn set <id> <reason> [| notes]",
"fwarn_help_syntax": "Usage: fwarn help <subcommand>",
"warn_list_syntax": "Usage: warn list [-all] [page]",
"warn_view_syntax": "Usage: warn view <id>",
"warn_ack_syntax": "Usage: warn ack <id>",
"warn_help_syntax": "Uwage: warn help <subcommand>",
"fwarn_add_syntax": "Usage: fwarn add <nick[!user@host]|=account> [@]<points> [~expiry] [sanctions] <:reason> [| notes]",
"fwarn_page_invalid": "Invalid page, must be a number 1 or greater.",
"fwarn_points_invalid": "Invalid points, must be a number above 0.",
"fwarn_expiry_invalid": "Invalid expiration amount, must be a number above 0.",
"fwarn_expiry_invalid_suffix": "Invalid expiration suffix, must use either d, h, or m.",
"fwarn_cannot_add": "Cannot add warning, double-check your parameters (the nick might be wrong or you are not joined to the channel).",
"fwarn_added": "Added warning {0}.",
@ -813,24 +820,32 @@
"fwarn_stasis_invalid": "Invalid stasis amount, specify sanction as stasis=number.",
"fwarn_deny_invalid": "Invalid denied commands, specify sanction as deny=command,command,command without spaces.",
"fwarn_deny_invalid_command": "Invalid command \"{0}\", specify sanction as deny=command,command,command without spaces.",
"fwarn_list_header": "{0} has {1} active warning points.",
"fwarn_list_header": "{0} has {1} active warning points. Warnings prefixed with \u0002!\u0002 are unacknowledged.",
"warn_list_header": "You have {0} active warning points. You must acknowledge all warnings prefixed with \u0002!\u0002 by using \"warn ack <id>\" before you can join games.",
"fwarn_list": "{0}{1}[#{2} {3}] to {4} by {5} - {6} ({7} points, {8}){9}",
"warn_list": "{0}{1}[#{2} {3}] {4} ({5} points, {6}){7}",
"fwarn_deleted": "deleted",
"fwarn_expired": "expired",
"fwarn_list_expired": "expired on {0}",
"fwarn_never_expires": "never expires",
"fwarn_list_footer": "More results are available, use fwarn list {0} to view them.",
"warn_list_footer": "More results are available, use warn list {0} to view them.",
"fwarn_list_empty": "No results.",
"fwarn_invalid_warning": "The specified warning id does not exist or you do not have permission to view it.",
"fwarn_view_header": "Warning #{0}, given to {1} on {2} by {3}. {4} points. {5}.",
"warn_view_header": "Warning #{0}, given on {1}. {2} points. {3}.",
"fwarn_view_active": "Currently active, {0}",
"fwarn_view_expires": "expires on {0}",
"fwarn_view_expired": "Expired on {0}",
"fwarn_view_deleted": "Deleted on {0} by {1}",
"fwarn_view_ack": "Warning has not yet been acknowledged.",
"warn_view_ack": "You have not yet acknowledge this warning. You must acknowledge this warning by using \"warn ack {0}\" before you can join games.",
"fwarn_view_sanctions": "Sanctions:",
"fwarn_view_stasis": "{0} games of stasis.",
"fwarn_view_stasis_sing": "1 game of stasis.",
"fwarn_view_stasis_plural": "{0} games of stasis.",
"fwarn_view_deny": "denied {0}.",
"fwarn_reason_required": "A public warning reason is required.",
"warn_unacked": "You have unacknowledged warnings and cannot join at this time. Use \"warn list\" to view them.",
"_": " vim: set sw=4 expandtab:"
}

View File

@ -84,10 +84,6 @@ if args.normal: normal = True
botconfig.DEBUG_MODE = debug_mode if not normal else False
botconfig.VERBOSE_MODE = verbose if not normal else False
# Initialize Database
db.init()
# Logger
# replace characters that can't be encoded with '?'

236
src/db.py
View File

@ -3,34 +3,37 @@ import src.settings as var
import sqlite3
import os
import json
from collections import defaultdict
# increment this whenever making a schema change so that the schema upgrade functions run on start
# they do not run by default for performance reasons
SCHEMA_VERSION = 1
conn = None
need_install = not os.path.isfile("data.sqlite3")
conn = sqlite3.connect("data.sqlite3")
with conn:
c = conn.cursor()
c.execute("PRAGMA foreign_keys = ON")
if need_install:
_install()
c.execute("PRAGMA user_version")
row = c.fetchone()
if row[0] == 0:
# new schema does not exist yet, migrate from old schema
# NOTE: game stats are NOT migrated to the new schema; the old gamestats table
# will continue to exist to allow queries against it, however given how horribly
# inaccurate the stats on it are, it would be a disservice to copy those inaccurate
# statistics over to the new schema which has the capability of actually being accurate.
_migrate()
elif row[0] < SCHEMA_VERSION:
_upgrade()
c.close()
def init():
global conn
need_install = not os.path.isfile("data.sqlite3")
conn = sqlite3.connect("data.sqlite3")
with conn:
del need_install, c
def init_vars():
with var.GRAVEYARD_LOCK:
c = conn.cursor()
c.execute("PRAGMA foreign_keys = ON")
if need_install:
_install()
c.execute("PRAGMA user_version")
row = c.fetchone()
if row[0] == 0:
# new schema does not exist yet, migrate from old schema
# NOTE: game stats are NOT migrated to the new schema; the old gamestats table
# will continue to exist to allow queries against it, however given how horribly
# inaccurate the stats on it are, it would be a disservice to copy those inaccurate
# statistics over to the new schema which has the capability of actually being accurate.
_migrate()
elif row[0] < SCHEMA_VERSION:
_upgrade()
c.execute("""SELECT
pl.account,
pl.hostmask,
@ -39,14 +42,37 @@ def init():
pe.deadchat,
pe.pingif,
pe.stasis_amount,
pe.stasis_expires
pe.stasis_expires,
COALESCE(at.flags, a.flags)
FROM person pe
JOIN person_player pp
ON pp.person = pe.id
JOIN player pl
ON pl.id = pp.player
LEFT JOIN access a
ON a.person = pe.id
LEFT JOIN access_template at
ON at.id = a.template
WHERE pl.active = 1""")
for (acc, host, notice, simple, dc, pi, stasis, stasisexp) in c:
var.SIMPLE_NOTIFY = set() # cloaks of people who !simple, who don't want detailed instructions
var.SIMPLE_NOTIFY_ACCS = set() # same as above, except accounts. takes precedence
var.PREFER_NOTICE = set() # cloaks of people who !notice, who want everything /notice'd
var.PREFER_NOTICE_ACCS = set() # Same as above, except accounts. takes precedence
var.STASISED = defaultdict(int)
var.STASISED_ACCS = defaultdict(int)
var.PING_IF_PREFS = {}
var.PING_IF_PREFS_ACCS = {}
var.PING_IF_NUMS = defaultdict(set)
var.PING_IF_NUMS_ACCS = defaultdict(set)
var.DEADCHAT_PREFS = set()
var.DEADCHAT_PREFS_ACCS = set()
var.FLAGS = defaultdict(str)
var.FLAGS_ACCS = defaultdict(str)
var.DENY = defaultdict(set)
var.DENY_ACCS = defaultdict(set)
for acc, host, notice, simple, dc, pi, stasis, stasisexp, flags in c:
if acc is not None:
if simple == 1:
var.SIMPLE_NOTIFY_ACCS.add(acc)
@ -59,6 +85,8 @@ def init():
var.PING_IF_NUMS_ACCS[pi].add(acc)
if dc == 1:
var.DEADCHAT_PREFS_ACCS.add(acc)
if flags:
var.FLAGS_ACCS[acc] = flags
elif host is not None:
if simple == 1:
var.SIMPLE_NOTIFY.add(host)
@ -71,6 +99,74 @@ def init():
var.PING_IF_NUMS[pi].add(host)
if dc == 1:
var.DEADCHAT_PREFS.add(host)
if flags:
var.FLAGS[host] = flags
c.execute("""SELECT
pl.account,
pl.hostmask,
ws.data
FROM warning w
JOIN warning_sanction ws
ON ws.warning = w.id
JOIN person pe
ON pe.id = w.target
JOIN person_player pp
ON pp.person = pe.id
JOIN player pl
ON pl.id = pp.player
WHERE
ws.sanction = 'deny command'
AND w.deleted = 0
AND (
w.expires IS NULL
OR w.expires > datetime('now')
)""")
for acc, host, command in c:
if acc is not None:
var.DENY_ACCS[acc].add(command)
if host is not None:
var.DENY[host].add(command)
init_vars()
def decrement_stasis(acc=None, hostmask=None):
peid, plid = _get_ids(acc, hostmask)
if (acc is not None or hostmask is not None) and peid is None:
return
sql = "UPDATE person SET stasis_amount = MAX(0, stasis_amount - 1)"
params = ()
if peid is not None:
sql += " WHERE id = ?"
params = (peid,)
with conn:
c = conn.cursor()
c.execute(sql, params)
def decrease_stasis(newamt, acc=None, hostmask=None):
peid, plid = _get_ids(acc, hostmask)
if peid is None:
return
if newamt < 0:
newamt = 0
with conn:
c = conn.cursor()
c.execute("""UPDATE person
SET stasis_amount = MIN(stasis_amount, ?)
WHERE id = ?""", (newamt, peid))
def expire_stasis():
with conn:
c = conn.cursor()
c.execute("""UPDATE person
SET
stasis_amount = 0,
stasis_expires = NULL
WHERE
stasis_expires IS NOT NULL
AND stasis_expires <= datetime('now')""")
def toggle_simple(acc, hostmask):
_toggle_thing("simple", acc, hostmask)
@ -84,9 +180,6 @@ def toggle_deadchat(acc, hostmask):
def set_pingif(val, acc, hostmask):
_set_thing("pingif", val, acc, hostmask, raw=False)
def set_stasis(val, acc, hostmask):
_set_thing("stasis_amount", val, acc, hostmask, raw=False)
def add_game(mode, size, started, finished, winner, players, options):
""" Adds a game record to the database.
@ -257,39 +350,6 @@ def get_game_totals(mode):
totals.append("\u0002{0}p\u0002: {1}".format(*row))
return "Total games ({0}) | {1}".format(total_games, ", ".join(totals))
def get_flags(acc, hostmask):
peid, plid = _get_ids(acc, hostmask)
c = conn.cursor()
c.execute("""SELECT COALESCE(at.flags, a.flags)
FROM access a
LEFT JOIN access_template at
ON at.id = a.template
WHERE a.person = ?""", (peid,))
row = c.fetchone()
if row is None:
return ""
return row[0]
def get_denied_commands(acc, hostmask):
peid, plid = _get_ids(acc, hostmask)
c = conn.cursor()
c.execute("""SELECT ws.data
FROM warning w
JOIN warning_sanction ws
ON ws.warning = w.id
WHERE
ws.sanction = 'deny command'
AND w.target = ?
AND w.deleted = 0
AND (
w.expires IS NULL
OR w.expires > datetime('now')
)""", (peid,))
cmds = set()
for row in c:
cmds.add(row[0])
return cmds
def get_warning_points(acc, hostmask):
peid, plid = _get_ids(acc, hostmask)
c = conn.cursor()
@ -305,6 +365,23 @@ def get_warning_points(acc, hostmask):
row = c.fetchone()
return row[0]
def has_unacknowledged_warnings(acc, hostmask):
peid, plid = _get_ids(acc, hostmask)
if peid is None:
return False
c = conn.cursor()
c.execute("""SELECT MIN(acknowledged)
FROM warning
WHERE
target = ?
AND deleted = 0
AND (
expires IS NULL
OR expires > datetime('now')
)""", (peid,))
row = c.fetchone()
return not bool(row[0])
def list_all_warnings(list_all=False, skip=0, show=0):
c = conn.cursor()
sql = """SELECT
@ -356,7 +433,7 @@ def list_all_warnings(list_all=False, skip=0, show=0):
"reason": row[9]})
return warnings
def list_warnings(acc, hostmask, list_all=False, skip=0, show=0):
def list_warnings(acc, hostmask, expired=False, deleted=False, skip=0, show=0):
peid, plid = _get_ids(acc, hostmask)
c = conn.cursor()
sql = """SELECT
@ -383,16 +460,16 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0):
WHERE
warning.target = ?
"""
if not list_all:
sql += """ AND deleted = 0
AND (
if not deleted:
sql += " AND deleted = 0"
if not expired:
sql += """ AND (
expires IS NULL
OR expires > datetime('now')
)
"""
sql += "ORDER BY warning.issued DESC\n"
)"""
sql += " ORDER BY warning.issued DESC"
if show > 0:
sql += "LIMIT {0} OFFSET {1}".format(show, skip)
sql += " LIMIT {0} OFFSET {1}".format(show, skip)
c.execute(sql, (botconfig.NICK, peid))
warnings = []
@ -410,7 +487,7 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0):
return warnings
def get_warning(warn_id, acc=None, hm=None):
pe, pl = _get_ids(acc, hm)
peid, plid = _get_ids(acc, hm)
c = conn.cursor()
sql = """SELECT
warning.id,
@ -486,13 +563,14 @@ def get_warning_sanctions(warn_id):
def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack):
teid, tlid = _get_ids(tacc, thm)
seid, slid = _get_ids(sacc, shm)
ack = 0 if need_ack else 1
with conn:
c = conn.cursor()
c.execute("""INSERT INTO warning
(
target, sender, amount,
issued, expires,
reasons, notes,
reason, notes,
acknowledged
)
VALUES
@ -501,7 +579,7 @@ def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack):
datetime('now'), ?,
?, ?,
?
)""", (teid, seid, amount, expires, reasons, notes, not need_ack))
)""", (teid, seid, amount, expires, reason, notes, ack))
return c.lastrowid
def add_warning_sanction(warning, sanction, data):
@ -512,6 +590,19 @@ def add_warning_sanction(warning, sanction, data):
VALUES
(?, ?, ?)""", (warning, sanction, data))
if sanction == "stasis":
c.execute("SELECT target FROM warning WHERE id = ?", (warning,))
peid = c.fetchone()[0]
c.execute("""UPDATE person
SET
stasis_amount = stasis_amount + ?,
stasis_expires = datetime(CASE WHEN stasis_expires IS NULL
OR stasis_expires <= datetime('now')
THEN 'now'
ELSE stasis_expires END,
'+{0} hours')
WHERE id = ?""".format(int(data)), (data, peid))
def del_warning(warning, acc, hm):
peid, plid = _get_ids(acc, hm)
with conn:
@ -533,6 +624,11 @@ def set_warning(warning, reason, notes):
SET reason = ?, notes = ?
WHERE id = ?""", (reason, notes, warning))
def acknowledge_warning(warning):
with conn:
c = conn.cursor()
c.execute("UPDATE warning SET acknowledged = 1 WHERE id = ?", (warning,))
def _upgrade():
# no upgrades yet, once there are some, add methods like _add_table(), _add_column(), etc.
# that check for the existence of that table/column/whatever and adds/drops/whatevers them

View File

@ -188,15 +188,13 @@ class cmd:
cli.notice(nick, messages["not_owner"])
return
# TODO: cache flags and cmds (below) on init, possibly store in var.USERS
# that would greatly reduce our db calls
flags = db.get_flags(acc, hostmask)
flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc]
is_full_admin = "F" in flags
if self.flag and (is_full_admin or is_owner):
adminlog(chan, rawnick, self.name, rest)
return self.func(*largs)
denied_cmds = db.get_denied_commands(acc, hostmask)
denied_cmds = var.DENY[hostmask] | var.DENY_ACCS[acc]
for command in self.cmds:
if command in denied_cmds:
if chan == nick:

View File

@ -1,5 +1,6 @@
import fnmatch
import re
import threading
from collections import defaultdict, OrderedDict
import botconfig
@ -195,10 +196,6 @@ TOTEM_CHANCES = { "death": ( 1 , 1 , 0
GAME_MODES = {}
GAME_PHASES = ("night", "day") # all phases that constitute "in game", game modes can extend this with custom phases
SIMPLE_NOTIFY = set() # cloaks of people who !simple, who don't want detailed instructions
SIMPLE_NOTIFY_ACCS = set() # same as above, except accounts. takes precedence
PREFER_NOTICE = set() # cloaks of people who !notice, who want everything /notice'd
PREFER_NOTICE_ACCS = set() # Same as above, except accounts. takes precedence
ACCOUNTS_ONLY = False # If True, will use only accounts for everything
DISABLE_ACCOUNTS = False # If True, all account-related features are disabled. Automatically set if we discover we do not have proper ircd support for accounts
@ -212,9 +209,6 @@ NICKSERV_REGAIN_COMMAND = "REGAIN {nick}"
CHANSERV = "ChanServ"
CHANSERV_OP_COMMAND = "OP {channel}"
STASISED = defaultdict(int)
STASISED_ACCS = defaultdict(int)
# TODO: move this to a game mode called "fixed" once we implement a way to randomize roles (and have that game mode be called "random")
DEFAULT_ROLE = "villager"
ROLE_INDEX = ( 4 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 15 , 16 , 18 , 20 , 21 , 23 , 24 )
@ -328,16 +322,9 @@ FORTUNE_CHANCE = 1/25
RULES = (botconfig.CHANNEL + " channel rules: http://wolf.xnrand.com/rules")
# pingif-related mappings
PING_IF_PREFS = {}
PING_IF_PREFS_ACCS = {}
PING_IF_NUMS = defaultdict(set)
PING_IF_NUMS_ACCS = defaultdict(set)
DEADCHAT_PREFS = set()
DEADCHAT_PREFS_ACCS = set()
GRAVEYARD_LOCK = threading.RLock()
WARNING_LOCK = threading.RLock()
WAIT_TB_LOCK = threading.RLock()
#TODO: move all of these to util.py or other files, as they are certainly NOT settings!
@ -386,7 +373,7 @@ def is_admin(nick, ident=None, host=None, acc=None):
if not acc:
acc = USERS[nick]["account"]
hostmask = nick + "!" + ident + "@" + host
flags = db.get_flags(acc, hostmask)
flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc]
return "F" in flags
def irc_lower(nick):

View File

@ -89,9 +89,6 @@ var.LAST_SAID_TIME = {}
var.GAME_START_TIME = datetime.now() # for idle checker only
var.CAN_START_TIME = 0
var.GRAVEYARD_LOCK = threading.RLock()
var.WARNING_LOCK = threading.RLock()
var.WAIT_TB_LOCK = threading.RLock()
var.STARTED_DAY_PLAYERS = 0
var.DISCONNECTED = {} # players who got disconnected
@ -361,6 +358,15 @@ def get_victim(cli, nick, victim, in_chan, self_in_list=False, bot_in_list=False
return
return pl[pll.index(tempvictim)] #convert back to normal casing
# wrapper around complete_match() used for any nick on the channel
def get_nick(cli, nick):
ul = [x for x in var.USERS]
ull = [x.lower() for x in var.USERS]
lnick, num_matches = complete_match(nick.lower(), ull)
if not lnick:
return None
return ul[ull.index(lnick)]
def get_roles(*roles):
all_roles = []
for role in roles:
@ -449,6 +455,10 @@ def sync_modes(cli):
mass_mode(cli, voices, other)
@cmd("refreshdb", flag="m", pm=True)
def refreshdb(cli, nick, chan, rest):
"""Updates our tracking vars to the current db state."""
db.init_vars()
@cmd("fdie", "fbye", flag="D", pm=True)
def forced_exit(cli, nick, chan, rest):
@ -1162,11 +1172,13 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True):
ident = var.USERS[player]["ident"]
host = var.USERS[player]["host"]
acc = var.USERS[player]["account"]
hostmask = player + "!" + ident + "@" + host
elif is_fake_nick(player) and botconfig.DEBUG_MODE:
# fakenick
ident = None
host = None
acc = None
hostmask = None
else:
return False # Not normal
if not acc or acc == "*" or var.DISABLE_ACCOUNTS:
@ -1183,6 +1195,11 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True):
"s" if stasis != 1 else ""))
return False
# don't check unacked warnings on fjoin
if who == player and db.has_unacknowledged_warnings(acc, hostmask):
cli.notice(player, messages["warn_unacked"])
return False
cmodes = [("+v", player)]
if var.PHASE == "none":
if var.AUTO_TOGGLE_MODES and player in var.USERS and var.USERS[player]["modes"]:
@ -1297,6 +1314,9 @@ def kill_join(cli, chan):
reset()
cli.msg(chan, msg)
cli.msg(chan, messages["game_idle_cancel"])
# use this opportunity to expire pending stasis
db.expire_stasis()
db.init_vars()
if var.AFTER_FLASTGAME is not None:
var.AFTER_FLASTGAME()
var.AFTER_FLASTGAME = None
@ -7907,19 +7927,15 @@ def decrement_stasis(nick=None):
acc = var.USERS[nick]["account"]
# decrement account stasis even if accounts are disabled
if acc in var.STASISED_ACCS:
var.STASISED_ACCS[acc] = max(0, var.STASISED_ACCS[acc] - 1)
db.set_stasis(var.STASISED_ACCS[acc], acc, None)
db.decrement_stasis(acc=acc)
for hostmask in var.STASISED:
if var.match_hostmask(hostmask, nick, ident, host):
var.STASISED[hostmask] = max(0, var.STASISED[hostmask] - 1)
db.set_stasis(var.STASISED[hostmask], None, hostmask)
db.decrement_stasis(hostmask=hostmask)
else:
for acc in var.STASISED_ACCS:
var.STASISED_ACCS[acc] = max(0, var.STASISED_ACCS[acc] - 1)
db.set_stasis(var.STASISED_ACCS[acc], acc, None)
for hostmask in var.STASISED:
var.STASISED[hostmask] = max(0, var.STASISED[hostmask] - 1)
db.set_stasis(var.STASISED[hostmask], None, hostmask)
db.decrement_stasis()
# Also expire any expired stasis and update our tracking vars
db.expire_stasis()
db.init_vars()
def parse_warning_target(target):
if target[0] == "=":
@ -7988,6 +8004,9 @@ def add_warning(target, amount, actor, reason, notes=None, expires=None, need_ac
for cmd in sanctions["deny"]:
db.add_warning_sanction(sid, "deny command", cmd)
# Update any tracking vars that may have changed due to this
db.init_vars()
return sid
@cmd("stasis", chan=True, pm=True)
@ -8000,14 +8019,226 @@ def stasis(cli, nick, chan, rest):
reply(cli, nick, chan, msg, prefix_nick=True)
@cmd("fstasis", flag="A", chan=True, pm=True)
def fstasis(cli, nick, chan, rest):
"""Removes or views stasis penalties."""
data = rest.split()
msg = None
if data:
lusers = {k.lower(): v for k, v in var.USERS.items()}
acc, hostmask = parse_warning_target(data[0])
cur = max(var.STASISED[hostmask], var.STASISED_ACCS[acc])
if len(data) == 1:
if acc is not None and var.STASISED_ACCS[acc] == cur:
plural = "" if cur == 1 else "s"
reply(cli, nick, chan, messages["account_in_stasis"].format(data[0], acc, cur, plural))
elif hostmask is not None and var.STASISED[hostmask] == cur:
plural = "" if cur == 1 else "s"
reply(cli, nick, chan, messages["hostmask_in_stasis"].format(data[0], hostmask, cur, plural))
elif acc is not None:
reply(cli, nick, chan, messages["account_not_in_stasis"].format(data[0], acc))
else:
reply(cli, nick, chan, messages["hostmask_not_in_stasis"].format(data[0], hostmask))
else:
try:
amt = int(data[1])
except ValueError:
reply(cli, nick, chan, messages["stasis_not_negative"])
return
if amt < 0:
reply(cli, nick, chan, messages["stasis_not_negative"])
return
elif amt > cur:
reply(cli, nick, chan, messages["stasis_cannot_increase"])
return
elif cur == 0:
if acc is not None:
reply(cli, nick, chan, messages["account_not_in_stasis"].format(data[0], acc))
return
else:
reply(cli, nick, chan, messages["hostmask_not_in_stasis"].format(data[0], hostmask))
return
db.decrease_stasis(amt, acc, hostmask)
db.init_vars()
if amt > 0:
plural = "" if amt == 1 else "s"
if acc is not None:
reply(cli, nick, chan, messages["fstasis_account_add"].format(data[0], acc, amt, plural))
else:
reply(cli, nick, chan, messages["fstasis_hostmask_add"].format(data[0], hostmask, amt, plural))
elif acc is not None:
reply(cli, nick, chan, messages["fstasis_account_remove"].format(data[0], acc))
else:
reply(cli, nick, chan, messages["fstasis_hostmask_remove"].format(data[0], hostmask))
elif var.STASISED or var.STASISED_ACCS:
stasised = {}
for hostmask in var.STASISED:
if var.DISABLE_ACCOUNTS:
stasised[hostmask] = var.STASISED[hostmask]
else:
stasised[hostmask+" (Host)"] = var.STASISED[hostmask]
if not var.DISABLE_ACCOUNTS:
for acc in var.STASISED_ACCS:
stasised[acc+" (Account)"] = var.STASISED_ACCS[acc]
msg = messages["currently_stasised"].format(", ".join(
"\u0002{0}\u0002 ({1})".format(usr, number)
for usr, number in stasised.items()))
reply(cli, nick, chan, msg)
else:
reply(cli, nick, chan, messages["noone_stasised"])
@cmd("warn", pm=True)
def warn(cli, nick, chan, rest):
"""View and acknowledge your warnings."""
# !warn list [all] - lists all active warnings, or all warnings if all passed
# !warn list [-all] [page] - lists all active warnings, or all warnings if all passed
# !warn view <id> - views details on warning id
# !warn ack <id> - acknowledges warning id
# Default if only !warn is given is to do !warn list.
pass
params = re.split(" +", rest)
try:
command = params.pop(0)
if command == "":
command = "list"
except IndexError:
command = "list"
if command not in ("list", "view", "ack", "help"):
reply(cli, nick, chan, messages["warn_usage"])
return
if command == "help":
try:
subcommand = params.pop(0)
except IndexError:
reply(cli, nick, chan, messages["warn_help_syntax"])
return
if subcommand not in ("list", "view", "ack", "help"):
reply(cli, nick, chan, messages["warn_usage"])
return
reply(cli, nick, chan, messages["warn_{0}_syntax".format(subcommand)])
return
if command == "list":
list_all = False
page = 1
try:
list_all = params.pop(0)
target = params.pop(0)
page = int(params.pop(0))
except IndexError:
pass
except ValueError:
reply(cli, nick, chan, messages["fwarn_page_invalid"])
return
try:
if list_all and list_all != "-all":
page = int(list_all)
list_all = False
elif list_all == "-all":
list_all = True
except ValueError:
reply(cli, nick, chan, messages["fwarn_page_invalid"])
return
acc, hm = parse_warning_target(nick)
warnings = db.list_warnings(acc, hm, expired=list_all, skip=(page-1)*10, show=11)
points = db.get_warning_points(acc, hm)
cli.notice(nick, messages["warn_list_header"].format(points))
i = 0
for warn in warnings:
i += 1
if (i == 11):
parts = []
if list_all:
parts.append("-all")
parts.append(str(page + 1))
cli.notice(nick, messages["warn_list_footer"].format(" ".join(parts)))
break
start = ""
end = ""
ack = ""
if warn["expires"] is not None:
if warn["expired"]:
expires = messages["fwarn_list_expired"].format(warn["expires"])
else:
expires = messages["fwarn_view_expires"].format(warn["expires"])
else:
expires = messages["fwarn_never_expires"]
if warn["expired"]:
start = "\u000314"
end = " [\u00037{0}\u000314]\u0003".format(messages["fwarn_expired"])
if not warn["ack"]:
ack = "\u0002!\u0002 "
cli.notice(nick, messages["warn_list"].format(
start, ack, warn["id"], warn["issued"], warn["reason"], warn["amount"], expires, end))
if i == 0:
cli.notice(nick, messages["fwarn_list_empty"])
return
if command == "view":
try:
warn_id = int(params.pop(0))
except (IndexError, ValueError):
reply(cli, nick, chan, messages["warn_view_syntax"])
return
acc, hm = parse_warning_target(nick)
warning = db.get_warning(warn_id, acc, hm)
if warning is None:
reply(cli, nick, chan, messages["fwarn_invalid_warning"])
return
if warning["expired"]:
expires = messages["fwarn_view_expired"].format(warning["expires"])
elif warning["expires"] is None:
expires = messages["fwarn_view_active"].format(messages["fwarn_never_expires"])
else:
expires = messages["fwarn_view_active"].format(messages["fwarn_view_expires"].format(warning["expires"]))
cli.notice(nick, messages["warn_view_header"].format(
warning["id"], warning["issued"], warning["amount"], expires))
cli.notice(nick, warning["reason"])
sanctions = []
if not warning["ack"]:
sanctions.append(messages["warn_view_ack"].format(warning["id"]))
if warning["sanctions"]:
sanctions.append(messages["fwarn_view_sanctions"])
if "stasis" in warning["sanctions"]:
if warning["sanctions"]["stasis"] != 1:
sanctions.append(messages["fwarn_view_stasis_plural"].format(warning["sanctions"]["stasis"]))
else:
sanctions.append(messages["fwarn_view_stasis_sing"])
if "deny" in warning["sanctions"]:
sanctions.append(messages["fwarn_view_deny"].format(", ".join(warning["sanctions"]["deny"])))
if sanctions:
cli.notice(nick, " ".join(sanctions))
return
if command == "ack":
try:
warn_id = int(params.pop(0))
except (IndexError, ValueError):
reply(cli, nick, chan, messages["warn_ack_syntax"])
return
acc, hm = parse_warning_target(nick)
warning = db.get_warning(warn_id, acc, hm)
if warning is None:
reply(cli, nick, chan, messages["fwarn_invalid_warning"])
return
db.acknowledge_warning(warn_id)
reply(cli, nick, chan, messages["fwarn_done"])
return
@cmd("fwarn", flag="A", pm=True)
def fwarn(cli, nick, chan, rest):
@ -8039,7 +8270,7 @@ def fwarn(cli, nick, chan, rest):
target = None
points = None
need_ack = False
expiry = None
expires = None
sanctions = {}
reason = None
notes = None
@ -8050,6 +8281,10 @@ def fwarn(cli, nick, chan, rest):
reply(cli, nick, chan, messages["fwarn_usage"])
return
if command not in ("list", "view", "add", "del", "set", "help"):
reply(cli, nick, chan, messages["fwarn_usage"])
return
if command == "help":
try:
subcommand = params.pop(0)
@ -8069,18 +8304,30 @@ def fwarn(cli, nick, chan, rest):
list_all = params.pop(0)
target = params.pop(0)
page = int(params.pop(0))
if list_all and list_all != "-all":
if target is not None:
page = int(target)
target = list_all
list_all = False
elif show_all == "-all":
list_all = True
except IndexError:
pass
except ValueError:
reply(cli, nick, chan, messages["fwarn_page_invalid"])
return
try:
if list_all and list_all != "-all":
if target is not None:
page = int(target)
target = list_all
list_all = False
elif list_all == "-all":
list_all = True
except ValueError:
reply(cli, nick, chan, messages["fwarn_page_invalid"])
return
try:
page = int(target)
target = None
except (TypeError, ValueError):
pass
if target is not None:
acc, hm = parse_warning_target(target)
if acc is None and hm is None:
@ -8088,7 +8335,7 @@ def fwarn(cli, nick, chan, rest):
return
warnings = db.list_warnings(acc, hm, list_all=list_all, skip=(page-1)*10, show=11)
points = db.get_warning_points(acc, hm)
reply(cli, nick, chan, messages["fwarn_list_header"].format(target, points))
cli.notice(nick, messages["fwarn_list_header"].format(target, points))
else:
warnings = db.list_all_warnings(list_all=list_all, skip=(page-1)*10, show=11)
@ -8102,12 +8349,18 @@ def fwarn(cli, nick, chan, rest):
if target is not None:
parts.append(target)
parts.append(str(page + 1))
reply(cli, nick, chan, messages["fwarn_list_footer"].format(" ".join(parts)))
cli.notice(nick, messages["fwarn_list_footer"].format(" ".join(parts)))
break
start = ""
end = ""
ack = ""
expires = warn["expires"] if warn["expires"] is not None else messages["fwarn_never_expires"]
if warn["expires"] is not None:
if warn["expired"]:
expires = messages["fwarn_list_expired"].format(warn["expires"])
else:
expires = messages["fwarn_view_expires"].format(warn["expires"])
else:
expires = messages["fwarn_never_expires"]
if warn["deleted"]:
start = "\u000314"
end = " [\u00034{0}\u000314]\u0003".format(messages["fwarn_deleted"])
@ -8116,11 +8369,11 @@ def fwarn(cli, nick, chan, rest):
end = " [\u00037{0}\u000314]\u0003".format(messages["fwarn_expired"])
if not warn["ack"]:
ack = "\u0002!\u0002 "
reply(cli, nick, chan, messages["fwarn_list"].format(
cli.notice(nick, messages["fwarn_list"].format(
start, ack, warn["id"], warn["issued"], warn["target"],
warn["sender"], warn["reason"], warn["amount"], expires, end))
if i == 0:
reply(cli, nick, chan, messages["fwarn_list_empty"])
cli.notice(nick, messages["fwarn_list_empty"])
return
if command == "view":
@ -8144,14 +8397,14 @@ def fwarn(cli, nick, chan, rest):
else:
expires = messages["fwarn_view_active"].format(messages["fwarn_view_expires"].format(warning["expires"]))
reply(cli, nick, chan, messages["fwarn_view_header"].format(
cli.notice(nick, messages["fwarn_view_header"].format(
warning["id"], warning["target"], warning["issued"], warning["sender"],
warning["amount"], expires))
reason = [warning["reason"]]
if warning["notes"] is not None:
reason.append(warning["notes"])
reply(cli, nick, chan, " | ".join(reason))
cli.notice(nick, " | ".join(reason))
sanctions = []
if not warning["ack"]:
@ -8159,11 +8412,14 @@ def fwarn(cli, nick, chan, rest):
if warning["sanctions"]:
sanctions.append(messages["fwarn_view_sanctions"])
if "stasis" in warning["sanctions"]:
sanctions.append(messages["fwarn_view_stasis"].format(warning["sanctions"]["stasis"]))
if warning["sanctions"]["stasis"] != 1:
sanctions.append(messages["fwarn_view_stasis_plural"].format(warning["sanctions"]["stasis"]))
else:
sanctions.append(messages["fwarn_view_stasis_sing"])
if "deny" in warning["sanctions"]:
sanctions.append(messages["fwarn_view_deny"].format(", ".join(warning["sanctions"]["deny"])))
if sanctions:
reply(cli, nick, chan, " ".join(sanctions))
cli.notice(nick, " ".join(sanctions))
return
if command == "del":
@ -8217,10 +8473,6 @@ def fwarn(cli, nick, chan, rest):
reply(cli, nick, chan, messages["fwarn_done"])
return
if command != "add":
reply(cli, nick, chan, messages["fwarn_usage"])
return
# command == "add"
while params:
p = params.pop(0)
@ -8256,7 +8508,7 @@ def fwarn(cli, nick, chan, rest):
if p == "~":
reply(cli, nick, chan, messages["fwarn_syntax"])
return
expiry = p[1:]
expires = p[1:]
else:
# sanctions are the only thing left here
sanc = p.split("=", 1)
@ -8303,12 +8555,22 @@ def fwarn(cli, nick, chan, rest):
# convert expires into a proper datetime
if expires is not None:
suffix = expires[-1]
try:
amount = int(expires[:-1])
except ValueError:
reply(cli, nick, chan, messages["fwarn_expiry_invalid"])
return
if amount <= 0:
reply(cli, nick, chan, messages["fwarn_expiry_invalid"])
return
if suffix == "d":
expires = datetime.now() + timedelta(days=expires[:-1])
expires = datetime.now() + timedelta(days=amount)
elif suffix == "h":
expires = datetime.now() + timedelta(hours=expires[:-1])
expires = datetime.now() + timedelta(hours=amount)
elif suffix == "m":
expires = datetime.now() + timedelta(minutes=expires[:-1])
expires = datetime.now() + timedelta(minutes=amount)
else:
reply(cli, nick, chan, messages["fwarn_expiry_invalid_suffix"])
return