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).
This commit is contained in:
Ryan Schmidt 2016-08-02 14:57:09 -07:00 committed by Emanuel Barry
parent c3698539c1
commit cd3f9fc345
8 changed files with 1017 additions and 825 deletions

View File

@ -790,17 +790,18 @@
"warn_view_syntax": "Usage: warn view <id>",
"warn_ack_syntax": "Usage: warn ack <id>",
"warn_help_syntax": "Usage: warn help <subcommand>",
"fwarn_add_syntax": "Usage: fwarn add <nick[!user@host]|=account> [@]<points> [~expiry] [sanctions] <:reason> [| notes]",
"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, 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 <id>\" 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:"
}

121
src/db.py
View File

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

View File

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

8
src/db/upgrade3.sql Normal file
View File

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

View File

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

View File

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

863
src/warnings.py Normal file
View File

@ -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 <id> - views details on warning id
# !warn ack <id> - 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 <id> - views details on warning id
# !fwarn del <id> - deletes warning id
# !fwarn set <id> [~expiry] [reason] [| notes]
# !fwarn add <nick> <points> [~expiry] [sanctions] [:]<reason> [| notes]
# e.g. !fwarn add lykos 1 ~30d deny=goat,gstats stasis=5 Spamming | I secretly just hate him
# nick => nick to warn. Can also be a hostmask in nick!user@host form. If nick is not online,
# interpreted as an account name. To specify an account if nick is online, use =account.
# points => Warning points, must be above 0
# ~expiry => Expiration time, must be suffixed with d (days), h (hours), or m (minutes)
# sanctions => list of sanctions. Valid sanctions are:
# deny: denies access to the listed commands
# stasis: gives the user stasis
# reason => Reason, required. If the first word of the reason is also a sanction, prefix it with :
# |notes => Secret notes, not shown to the user (only shown if viewing the warning in PM)
# If specified, must be prefixed with |. This means | is not a valid character for use
# in reasons (no escaping is performed).
params = 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:

View File

@ -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 <id> - views details on warning id
# !warn ack <id> - 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 <id> - views details on warning id
# !fwarn del <id> - deletes warning id
# !fwarn set <id> [~expiry] [reason] [| notes]
# !fwarn add <nick> [@]<points> [~expiry] [sanctions] <:reason> [| notes]
# e.g. !fwarn add lykos @1 ~30d deny=goat,gstats stasis=5 :Spamming | I secretly just hate him
# nick => nick to warn. Can also be a hostmask in nick!user@host form. If nick is not online,
# interpreted as an account name. To specify an account if nick is online, use =account.
# @ => 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)