From 2d2ce6483abb122ba913ff9f29b8cf1d1d2a8001 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Sun, 28 Feb 2016 20:03:32 -0600 Subject: [PATCH 01/10] Initial work on new schema and warning system Still very WIP and not completed, bot likely doesn't even run. --- messages/en.json | 21 + src/__init__.py | 3 +- src/db.py | 550 ++++++++++++++++++++++++++ src/db.sql | 171 ++++++++ src/decorators.py | 62 +-- src/migrate.sql | 214 ++++++++++ src/settings.py | 471 ++++------------------ src/wolfgame.py | 973 +++++++++++++++++++++------------------------- 8 files changed, 1487 insertions(+), 978 deletions(-) create mode 100644 src/db.py create mode 100644 src/db.sql create mode 100644 src/migrate.sql diff --git a/messages/en.json b/messages/en.json index 84965c7..b5aeb60 100644 --- a/messages/en.json +++ b/messages/en.json @@ -796,6 +796,27 @@ "villagergame_win": "Game over! The villagers come to their senses and realize there are actually no wolves, and live in harmony forevermore. Everybody wins.", "villagergame_nope": "Game over! The villagers decided incorrectly that there are actually no wolves, allowing the wolves to slaughter the remainder of them in their sleep with impunity.", "stop_bot_ingame_safeguard": "Warning: A game is currently running. If you want to {what} the bot anyway, use \"{prefix}{cmd} -force\".", + "fwarn_usage": "Usage: fwarn list|view|add|del|help. See fwarn help for more details.", + "fwarn_list_syntax": "Usage: fwarn list [-all] [nick[!user@host]|=account] [page]", + "fwarn_view_syntax": "Usage: fwarn view ", + "fwarn_del_syntax": "Usage: fwarn del ", + "fwarn_help_syntax": "Usage: fwarn help ", + "fwarn_add_syntax": "Usage: fwarn add [@] [~expiry] [sanctions] <:reason> [|notes]", + "fwarn_page_invalid": "Invalid page, must be a number 1 or greater.", + "fwarn_points_invalid": "Invalid points, must be a number above 0.", + "fwarn_expiry_invalid_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_sanction_invalid": "Invalid sanction, can be either deny or stasis.", + "fwarn_stasis_invalid": "Invalid stasis amount, specify sanction as stasis=number.", + "fwarn_deny_invalid": "Invalid denied commands, specify sanction as deny=command,command,command without spaces.", + "fwarn_deny_invalid_command": "Invalid command \"{0}\", specify sanction as deny=command,command,command without spaces.", + "fwarn_list_header": "{0} has {1} active warning points.", + "fwarn_list": "{0}{1}[#{2} {3}] {4} by {5} - {6} ({7}, {8}){9}", + "fwarn_deleted": "deleted", + "fwarn_expired": "expired", + "fwarn_never_expires": "never expires", + "fwarn_list_footer": "More results are available, use fwarn list {0} to view them.", + "fwarn_list_empty": "No results.", "_": " vim: set sw=4 expandtab:" } diff --git a/src/__init__.py b/src/__init__.py index ac5b109..ac53da1 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -4,6 +4,7 @@ import time import botconfig import src.settings as var +from src import db # Segue to logger, since src.gamemodes requires it # TODO: throw this into a logger.py perhaps so we aren't breaking up imports with non-import stuff @@ -85,7 +86,7 @@ botconfig.VERBOSE_MODE = verbose if not normal else False # Initialize Database -var.init_db() +db.init() # Logger diff --git a/src/db.py b/src/db.py new file mode 100644 index 0000000..29a052c --- /dev/null +++ b/src/db.py @@ -0,0 +1,550 @@ +import src.settings as var +import sqlite3 +import os +import json + +# 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 + +def init(): + global conn + 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.execute("""SELECT + pl.account, + pl.hostmask, + pe.notice, + pe.simple, + pe.deadchat, + pe.pingif, + pe.stasis_amount, + pe.stasis_expires + FROM person pe + JOIN person_player pp + ON pp.person = pe.id + JOIN player pl + ON pl.id = pp.player + WHERE pl.active = 1""") + for (acc, host, notice, simple, dc, pi, stasis, stasisexp) in c: + if acc is not None: + if simple == 1: + var.SIMPLE_NOTIFY_ACCS.add(acc) + if notice == 1: + var.PREFER_NOTICE_ACCS.add(acc) + if stasis > 0: + var.STASISED_ACCS[acc] = stasis + if pi is not None and pi > 0: + var.PING_IF_PREFS_ACCS[acc] = pi + var.PING_IF_NUMS_ACCS[pi].add(acc) + if dc == 1: + var.DEADCHAT_PREFS_ACCS.add(acc) + elif host is not None: + if simple == 1: + var.SIMPLE_NOTIFY.add(host) + if notice == 1: + var.PREFER_NOTICE.add(host) + if stasis > 0: + var.STASISED[host] = stasis + if pi is not None and pi > 0: + var.PING_IF_PREFS[host] = pi + var.PING_IF_NUMS[pi].add(host) + if dc == 1: + var.DEADCHAT_PREFS.add(host) + +def toggle_simple(acc, hostmask): + _toggle_thing("simple", acc, hostmask) + +def toggle_notice(acc, hostmask): + _toggle_thing("notice", acc, hostmask) + +def toggle_deadchat(acc, hostmask): + _toggle_thing("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. + + mode: Game mode (string) + size: Game size on start (int) + started: Time when game started (timestamp) + finished: Time when game ended (timestamp) + winner: Winning team (string) + players: List of players (sequence of dict, described below) + options: Game options (role reveal, stats type, etc., freeform dict) + + Players dict format: + { + nick: "Nickname" + account: "Account name" (or None, "*" is converted to None) + ident: "Ident" + host: "Host" + role: "role name" + templates: ["template names", ...] + special: ["special qualities", ... (lover, entranced, etc.)] + won: True/False + iwon: True/False + dced: True/False + } + """ + + if mode == "roles": + # Do not record stats for games with custom roles + return + + # Normalize players dict + for p in players: + if p["account"] == "*": + p["account"] = None + p["hostmask"] = "{0}!{1}@{2}".format(p["nick"], p["ident"], p["host"]) + c = conn.cursor() + p["personid"], p["playerid"] = _get_ids(p["account"], p["hostmask"], add=True) + with conn: + c = conn.cursor() + if winner.startswith("@"): + # fool won, convert the nick portion into a player id + for p in players: + if p["nick"] == winner[1:]: + winner = "@" + p["playerid"] + break + else: + # invalid winner? We can't find the fool's nick in the player list + # maybe raise an exception here instead of silently failing + return + + c.execute("""INSERT INTO game (gamemode, options, started, finished, gamesize, winner) + VALUES (?, ?, ?, ?, ?, ?)""", (mode, json.dumps(options), started, finished, size, winner)) + gameid = c.lastrowid + for p in players: + c.execute("""INSERT INTO game_player (game, player, team_win, indiv_win, dced) + VALUES (?, ?, ?, ?, ?)""", (gameid, p["playerid"], p["won"], p["iwon"], p["dced"])) + gpid = c.lastrowid + c.execute("""INSERT INTO game_player_role (game_player, role, special) + VALUES (?, ?, 0)""", (gpid, p["role"])) + for tpl in p["templates"]: + c.execute("""INSERT INTO game_player_role (game_player, role, special) + VALUES (?, ?, 0)""", (gpid, tpl)) + for sq in p["special"]: + c.execute("""INSERT INTO game_player_role (game_player, role, special) + VALUES (?, ?, 1)""", (gpid, sq)) + +def get_player_stats(acc, hostmask, role): + peid, plid = _get_ids(acc, hostmask) + if not _total_games(peid): + return "\u0002{0}\u0002 has not played any games.".format(acc if acc and acc != "*" else hostmask) + c = conn.cursor() + c.execute("""SELECT + gpr.role AS role, + SUM(gp.team_win) AS team, + SUM(gp.indiv_win) AS indiv, + COUNT(1) AS total + FROM person pe + JOIN person_player pmap + ON pmap.person = pe.id + JOIN game_player gp + ON gp.player = pmap.player + JOIN game_player_role gpr + ON gpr.game_player = gp.id + AND gpr.role = ? + WHERE pe.id = ? + GROUP BY role""", (role, peid)) + row = c.fetchone() + name = _get_display_name(peid) + if row: + msg = "\u0002{0}\u0002 as \u0002{1}\u0002 | Team wins: {2} (%d%%), Individual wins: {3} (%d%%), Total games: {4}.".format(name, *row) + return msg % (round(row[1]/row[3] * 100), round(row[2]/row[3] * 100)) + return "No stats for \u0002{0}\u0002 as \u0002{1}\u0002.".format(name, role) + +def get_player_totals(acc, hostmask): + peid, plid = _get_ids(acc, hostmask) + total_games = _total_games(peid) + if not total_games: + return "\u0002{0}\u0002 has not played any games.".format(acc if acc and acc != "*" else hostmask) + c = conn.cursor() + c.execute("""SELECT + gpr.role AS role, + COUNT(1) AS total + FROM person pe + JOIN person_player pmap + ON pmap.person = pe.id + JOIN game_player gp + ON gp.player = pmap.player + JOIN game_player_role gpr + ON gpr.game_player = gp.id + WHERE pe.id = ? + GROUP BY role""", (peid,)) + tmp = {} + totals = [] + for row in c: + tmp[row[0]] = row[1] + order = var.role_order() + name = _get_display_name(peid) + #ordered role stats + totals = ["\u0002{0}\u0002: {1}".format(r, tmp[r]) for r in order if r in tmp] + #lover or any other special stats + totals += ["\u0002{0}\u0002: {1}".format(r, t) for r, t in tmp.items() if r not in order] + return "\u0002{0}\u0002's totals | \u0002{1}\u0002 games | {2}".format(name, total_games, var.break_long_message(totals, ", ")) + +def get_game_stats(mode, size): + c = conn.cursor() + c.execute("SELECT COUNT(1) FROM game WHERE gamemode = ? AND gamesize = ?", (mode, size)) + total_games = c.fetchone()[0] + if not total_games: + return "No stats for \u0002{0}\u0002 player games.".format(size) + c.execute("""SELECT + CASE substr(winner, 1, 1) + WHEN '@' THEN 'fools' + ELSE winner END AS team, + COUNT(1) AS games, + CASE winner + WHEN 'villagers' THEN 0 + WHEN 'wolves' THEN 1 + ELSE 2 END AS ord + FROM game + WHERE + gamemode = ? + AND gamesize = ? + AND winner IS NOT NULL + GROUP BY team + ORDER BY ord ASC, team ASC""", (mode, size)) + msg = "\u0002{0}\u0002 player games | {1}" + bits = [] + for row in c: + bits.append("%s wins: %d (%d%%)" % (var.singular(row[0]), row[1], round(row[1]/total_games * 100))) + bits.append("total games: {0}".format(total_games)) + return msg.format(size, ", ".join(bits)) + +def get_game_totals(mode): + c = conn.cursor() + c.execute("SELECT COUNT(1) FROM game WHERE gamemode = ?", (mode,)) + total_games = c.fetchone()[0] + if not total_games: + return "No games have been played in the {0} game mode.".format(mode) + c.execute("""SELECT + gamesize, + COUNT(1) AS games + FROM game + WHERE gamemode = ? + GROUP BY gamesize + ORDER BY gamesize ASC""", (mode,)) + totals = [] + for row in c: + 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() + 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() + c.execute("""SELECT COALESCE(SUM(amount), 0) + FROM warning + WHERE + target = ? + AND deleted = 0 + AND ( + expires IS NULL + OR expires > datetime('now') + )""", (peid,)) + row = c.fetchone() + return row[0] + +def list_all_warnings(list_all=False, skip=0, show=0): + c = conn.cursor() + sql = """SELECT + warning.id, + COALESCE(plt.account, plt.hostmask) AS target, + COALESCE(pls.account, pls.hostmask, ?) AS sender, + warning.amount, + warning.issued, + warning.expires, + CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') + THEN 1 ELSE 0 END AS expired, + warning.acknowledged, + warning.deleted, + warning.reason + JOIN person pet + ON pet.id = warning.target + JOIN player plt + ON plt.id = pet.primary_player + LEFT JOIN pes + ON pes.id = warning.sender + LEFT JOIN pls + ON pls.id = pes.primary_player + """ + if not list_all: + sql += """WHERE + deleted = 0 + AND ( + expires IS NULL + OR expires > datetime('now') + ) + """ + sql += "ORDER BY warning.issued DESC\n" + if show > 0: + sql += "LIMIT {0} OFFSET {1}".format(show, skip) + + c.execute(sql, (botconfig.NICK,)) + warnings = [] + for row in c: + warnings.append({"id": row[0], + "target": row[1], + "sender": row[2], + "amount": row[3], + "issued": row[4], + "expires": row[5], + "expired": row[6], + "ack": row[7], + "deleted": row[8]}, + "reason": row[9]) + return warnings + +def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): + peid, plid = _get_ids(acc, hostmask) + c = conn.cursor() + sql = """SELECT + warning.id, + COALESCE(plt.account, plt.hostmask) AS target, + COALESCE(pls.account, pls.hostmask, ?) AS sender, + warning.amount, + warning.issued, + warning.expires, + CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') + THEN 1 ELSE 0 END AS expired, + warning.acknowledged, + warning.deleted, + warning.reason + JOIN person pet + ON pet.id = warning.target + JOIN player plt + ON plt.id = pet.primary_player + LEFT JOIN pes + ON pes.id = warning.sender + LEFT JOIN pls + ON pls.id = pes.primary_player + WHERE + warning.target = ? + """ + if not list_all: + sql += """ AND deleted = 0 + AND ( + expires IS NULL + OR expires > datetime('now') + ) + """ + sql += "ORDER BY warning.issued DESC\n" + if show > 0: + sql += "LIMIT {0} OFFSET {1}".format(show, skip) + + c.execute(sql, (botconfig.NICK, peid)) + warnings = [] + for row in c: + warnings.append({"id": row[0], + "target": row[1], + "sender": row[2], + "amount": row[3], + "issued": row[4], + "expires": row[5], + "expired": row[6], + "ack": row[7], + "deleted": row[8], + "reason": row[9]}) + return warnings + +def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack): + teid, tlid = _get_ids(tacc, thm) + seid, slid = _get_ids(sacc, shm) + c = conn.cursor() + c.execute("""INSERT INTO warning + ( + target, sender, amount, + issued, expires, + reasons, notes, + acknowledged + ) + VALUES + ( + ?, ?, ?, + datetime('now'), ?, + ?, ?, + ? + )""", (teid, seid, amount, expires, reasons, notes, not need_ack)) + return c.lastrowid + +def add_warning_sanction(warning, sanction, data): + c = conn.cursor() + c.execute("""INSERT INTO warning_sanction + (warning, sanction, data) + VALUES + (?, ?, ?)""", (warning, sanction, data)) + +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 + # as needed. We can't do this purely in SQL because sqlite lacks a scripting-level IF statement. + pass + +def _migrate(): + dn = os.path.dirname(__file__) + with conn, open(os.path.join(dn, "db.sql"), "rt") as f1, open(os.path.join(dn, "migrate.sql"), "rt") as f2: + c = conn.cursor() + ####################################################### + # Step 1: install the new schema (from db.sql script) # + ####################################################### + c.executescript(f1.read()) + + ################################################################ + # Step 2: migrate relevant info from the old schema to the new # + ################################################################ + c.executescript(f2.read()) + + ###################################################################### + # Step 3: Indicate we have updated the schema to the current version # + ###################################################################### + c.execute("PRAGMA user_version = " + str(SCHEMA_VERSION)) + +def _install(): + dn = os.path.dirname(__file__) + with conn, open(os.path.join(dn, "db.sql"), "rt") as f1: + c = conn.cursor() + c.executescript(f1.read()) + c.execute("PRAGMA user_version = " + str(SCHEMA_VERSION)) + +def _get_ids(acc, hostmask, add=False): + c = conn.cursor() + if acc == "*": + acc = None + if acc is None and hostmask is None: + return (None, None) + elif acc is None: + c.execute("""SELECT pe.id, pl.id + FROM player pl + JOIN person_player pp + ON pp.player = pl.id + JOIN person pe + ON pe.id = pp.person + WHERE + pl.account IS NULL + AND pl.hostmask = ? + AND pl.active = 1""", (hostmask,)) + else: + hostmask = None + c.execute("""SELECT pe.id, pl.id + FROM player pl + JOIN person_player pp + ON pp.player = pl.id + JOIN person pe + ON pe.id = pp.person + WHERE + pl.account = ? + AND pl.hostmask IS NULL + AND pl.active = 1""", (acc,)) + row = c.fetchone() + peid = None + plid = None + if row: + peid, plid = row + elif add: + with conn: + c.execute("INSERT INTO player (account, hostmask) VALUES (?, ?)", (acc, hostmask)) + plid = c.lastrowid + c.execute("INSERT INTO person (primary_player) VALUES (?)", (plid,)) + peid = c.lastrowid + c.execute("INSERT INTO person_player (person, player) VALUES (?, ?)", (peid, plid)) + return (peid, plid) + +def _get_display_name(peid): + if peid is None: + return None + c = conn.cursor() + c.execute("""SELECT COALESCE(pp.account, pp.hostmask) + FROM person pe + JOIN player pp + ON pp.id = pe.primary_player + WHERE pe.id = ?""", (peid,)) + return c.fetchone()[0] + +def _total_games(peid): + if peid is None: + return 0 + c = conn.cursor() + c.execute("""SELECT COUNT(DISTINCT gp.game) + FROM person pe + JOIN person_player pmap + ON pmap.person = pe.id + JOIN game_player gp + ON gp.player = pmap.player + WHERE + pe.id = ?""", (peid,)) + # aggregates without GROUP BY always have exactly one row, + # so no need to check for None here + return c.fetchone()[0] + +def _set_thing(thing, val, acc, hostmask, raw=False): + with conn: + c = conn.cursor() + peid, plid = _get_ids(acc, hostmask, add=True) + if raw: + params = (peid,) + else: + params = (val, peid) + val = "?" + c.execute("""UPDATE person SET {0} = {1} WHERE id = ?""".format(thing, val), params) + +def _toggle_thing(thing, acc, hostmask): + _set_thing(thing, "CASE {0} WHEN 1 THEN 0 ELSE 1 END".format(thing), acc, hostmask, raw=True) + +# vim: set expandtab:sw=4:ts=4: diff --git a/src/db.sql b/src/db.sql new file mode 100644 index 0000000..e3553a0 --- /dev/null +++ b/src/db.sql @@ -0,0 +1,171 @@ +-- Base schema, when editing be sure to increment the SCHEMA_VERSION in src/db.py +-- Additionally, add the appropriate bits to the update function, as this script +-- does not perform alters on already-existing tables + +-- 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 ( + id INTEGER PRIMARY KEY, + -- NickServ account name, or NULL if this player is based on a hostmask + account TEXT, + -- Hostmask for the player, if not based on an account (NULL otherwise) + hostmask TEXT, + -- If a player entry needs to be retired (for example, an account expired), + -- setting this to 0 allows for that entry to be re-used without corrupting old stats/logs + active BOOLEAN NOT NULL DEFAULT 1 +); + +CREATE INDEX IF NOT EXISTS player_idx ON player (account, hostmask, active); + +-- 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 ( + id INTEGER PRIMARY KEY, + -- Primary player for this person + primary_player INTEGER NOT NULL UNIQUE REFERENCES player(id), + -- If 1, the bot will notice the player instead of sending privmsgs + notice BOOLEAN NOT NULL DEFAULT 0, + -- If 1, the bot will send simple role notifications to the player + simple BOOLEAN NOT NULL DEFAULT 0, + -- If 1, the bot will automatically join the player to deadchat upon them dying + deadchat BOOLEAN NOT NULL DEFAULT 1, + -- Pingif preference for the person, or NULL if they do not wish to be pinged + pingif INTEGER, + -- Amount of stasis this person has (stasis prevents them from joining games while active) + -- each time a game is started, this is decremented by 1, to a minimum of 0 + stasis_amount INTEGER NOT NULL DEFAULT 0, + -- When the given stasis expires, represented in 'YYYY-MM-DD HH:MM:SS' format + stasis_expires DATETIME +); + +-- A person can have multiple attached players, however each player can be attached +-- to only exactly one person +CREATE TABLE IF NOT EXISTS person_player ( + person INTEGER NOT NULL REFERENCES person(id), + player INTEGER NOT NULL UNIQUE REFERENCES player(id) +); + +CREATE INDEX IF NOT EXISTS person_player_idx ON person_player (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 ( + id INTEGER PRIMARY KEY, + -- The target (recipient) of the warning + target INTEGER NOT NULL REFERENCES person(id), + -- The person who gave out the warning, or NULL if it was automatically generated + sender INTEGER REFERENCES person(id), + -- Number of warning points + amount INTEGER NOT NULL, + -- When the warning was issued ('YYYY-MM-DD HH:MM:SS') + issued DATETIME NOT NULL, + -- When the warning expires ('YYYY-MM-DD HH:MM:SS') or NULL if it never expires + expires DATETIME, + -- Reason for the warning (shown to the target) + -- Can be edited after the warning is issued + reason TEXT NOT NULL, + -- Optonal notes for the warning (only visible to admins) + -- Can be edited after the warning is issued + notes TEXT, + -- Set to 1 if the warning was acknowledged by the target + acknowledged BOOLEAN NOT NULL DEFAULT 0, + -- Set to 1 if the warning was rescinded by an admin before it expired + deleted BOOLEAN NOT NULL DEFAULT 0, + -- If the warning was rescinded, this tracks by whom + deleted_by INTEGER REFERENCES person(id), + -- If the warning was rescinded, this tracks when that happened ('YYYY-MM-DD HH:MM:SS') + deleted_on DATETIME +); + +CREATE INDEX IF NOT EXISTS warning_idx ON warning (target, 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 ( + -- The warning this sanction is attached to + warning INTEGER NOT NULL REFERENCES warning(id), + -- The type of sanction this is + sanction TEXT NOT NULL, + -- If the sanction type has additional data attached, it is listed here + data TEXT +); + +-- A running tally of all games played, game stats are aggregated from this table +-- 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 ( + id INTEGER PRIMARY KEY, + -- The gamemode played + gamemode TEXT NOT NULL, + -- Game options (role reveal, stats type, etc.), stored as JSON string + -- The json1 extension can be loaded into sqlite to allow for easy querying of these values + -- lykos itself does not make use of this field when calculating stats at this time + options TEXT, + -- When the game was started + started DATETIME NOT NULL, + -- When the game was finished + finished DATETIME NOT NULL, + -- Game size (at game start) + gamesize INTEGER NOT NULL, + -- Winning team (NULL if no winner) + winner TEXT +); + +CREATE INDEX IF NOT EXISTS game_idx ON game (gamemode, gamesize); + +-- List of people who played in each game +CREATE TABLE IF NOT EXISTS game_player ( + id INTEGER PRIMARY KEY, + game INTEGER NOT NULL REFERENCES game(id), + player INTEGER NOT NULL REFERENCES player(id), + -- 1 if the player has a team win for this game + team_win BOOLEAN NOT NULL, + -- 1 if the player has an individual win for this game + indiv_win BOOLEAN NOT NULL, + -- 1 if the player died due to a dc (kick, quit, idled out) + 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); + +-- 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 ( + game_player INTEGER NOT NULL REFERENCES game_player(id), + -- Name of the role or other quality recorded + role TEXT NOT NULL, + -- 1 if role is a special quality instead of an actual role/template name + special BOOLEAN NOT NULL +); + +CREATE INDEX IF NOT EXISTS 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 ( + id INTEGER PRIMARY KEY, + -- Template name, for display purposes + name TEXT NOT NULL, + -- Flags this template grants + flags TEXT +); + +-- Access control, owners still need to be specified in botconfig, but everyone else goes here +CREATE TABLE IF NOT EXISTS access ( + person INTEGER NOT NULL PRIMARY KEY REFERENCES person(id), + -- Template to base this person's access on, or NULL if it is not based on a template + template INTEGER REFERENCES access_template(id), + -- If template is NULL, this is the list of flags that will be used + -- Has no effect if template is not NULL + flags TEXT +); + +-- Used to hold state between restarts +CREATE TABLE IF NOT EXISTS pre_restart_state ( + -- List of players to ping after the bot comes back online + players TEXT +); diff --git a/src/decorators.py b/src/decorators.py index aefc26a..54341d6 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -66,12 +66,12 @@ class handle_error: cli.msg(botconfig.DEV_CHANNEL, " ".join((msg, url))) class cmd: - def __init__(self, *cmds, raw_nick=False, admin_only=False, owner_only=False, + def __init__(self, *cmds, raw_nick=False, flag=None, owner_only=False, chan=True, pm=False, playing=False, silenced=False, phases=(), roles=()): self.cmds = cmds self.raw_nick = raw_nick - self.admin_only = admin_only + self.flag = flag self.owner_only = owner_only self.chan = chan self.pm = pm @@ -88,7 +88,7 @@ class cmd: for name in cmds: for func in COMMANDS[name]: if (func.owner_only != owner_only or - func.admin_only != admin_only): + func.flag != flag): raise ValueError("unmatching protection levels for " + func.name) COMMANDS[name].append(self) @@ -125,7 +125,7 @@ class cmd: if not self.chan and chan != nick: return # channel command, not allowed - if chan.startswith("#") and chan != botconfig.CHANNEL and not (self.admin_only or self.owner_only): + if chan.startswith("#") and chan != botconfig.CHANNEL and not (self.flag or self.owner_only): if "" in self.cmds: return # don't have empty commands triggering in other channels for command in self.cmds: @@ -138,6 +138,7 @@ class cmd: acc = var.USERS[nick]["account"] else: acc = None + hostmask = nick + "!" + ident + "@" + host if "" in self.cmds: return self.func(*largs) @@ -186,48 +187,23 @@ class cmd: cli.notice(nick, messages["not_owner"]) return - if var.is_admin(nick, ident, host): - if self.admin_only: - adminlog(chan, rawnick, self.name, rest) + # TODO: cache flags and cmds (below) on init, possibly store in var.USERS + # that would greatly reduce our db calls + flags = db.get_flags(acc, hostmask) + if self.flag and self.flag in flags: + adminlog(chan, rawnick, self.name, rest) return self.func(*largs) - if not var.DISABLE_ACCOUNTS and acc: - if acc in var.DENY_ACCOUNTS: - for command in self.cmds: - if command in var.DENY_ACCOUNTS[acc]: - if chan == nick: - pm(cli, nick, messages["invalid_permissions"]) - else: - cli.notice(nick, messages["invalid_permissions"]) - return + denied_cmds = db.get_denied_commands(acc, hostmask) + for command in self.cmds: + if command in denied_commands: + if chan == nick: + pm(cli, nick, messages["invalid_permissions"]) + else: + cli.notice(nick, messages["invalid_permissions"]) + return - if acc in var.ALLOW_ACCOUNTS: - for command in self.cmds: - if command in var.ALLOW_ACCOUNTS[acc]: - if self.admin_only: - adminlog(chan, rawnick, self.name, rest) - return self.func(*largs) - - if host: - for pattern in var.DENY: - if var.match_hostmask(pattern, nick, ident, host): - for command in self.cmds: - if command in var.DENY[pattern]: - if chan == nick: - pm(cli, nick, messages["invalid_permissions"]) - else: - cli.notice(nick, messages["invalid_permissions"]) - return - - for pattern in var.ALLOW: - if var.match_hostmask(pattern, nick, ident, host): - for command in self.cmds: - if command in var.ALLOW[pattern]: - if self.admin_only: - adminlog(chan, rawnick, self.name, rest) - return self.func(*largs) - - if self.admin_only: + if self.flag: if chan == nick: pm(cli, nick, messages["not_an_admin"]) else: diff --git a/src/migrate.sql b/src/migrate.sql new file mode 100644 index 0000000..d422dda --- /dev/null +++ b/src/migrate.sql @@ -0,0 +1,214 @@ +-- First, create our player entries +INSERT INTO player ( + account, + hostmask, + active +) +SELECT DISTINCT + account, + NULL, + 1 +FROM ( + SELECT player AS account FROM rolestats + UNION ALL + SELECT acc AS account FROM allowed_accs + UNION ALL + SELECT user AS account FROM deadchat_prefs WHERE is_account = 1 + UNION ALL + SELECT acc AS account FROM denied_accs + UNION ALL + SELECT user AS account FROM pingif_prefs WHERE is_account = 1 + UNION ALL + SELECT acc AS account FROM prefer_notice_acc + UNION ALL + SELECT acc AS account FROM simple_role_accs + UNION ALL + SELECT acc AS account FROM stasised_accs +) t1 +UNION ALL +SELECT DISTINCT + NULL, + hostmask, + 1 +FROM ( + SELECT cloak AS hostmask FROM allowed + UNION ALL + SELECT user AS hostmask FROM deadchat_prefs WHERE is_account = 0 + UNION ALL + SELECT cloak AS hostmask FROM denied + UNION ALL + SELECT user AS hostmask FROM pingif_prefs WHERE is_account = 0 + UNION ALL + SELECT cloak AS hostmask FROM prefer_notice + UNION ALL + SELECT cloak AS hostmask FROM simple_role_notify + UNION ALL + SELECT cloak AS hostmask FROM stasised +) t2; + +-- Create our person entries (we assume a 1:1 person:player mapping for migration) +INSERT INTO person ( + primary_player, + notice, + simple, + deadchat, + pingif, + stasis_amount, + stasis_expires +) +SELECT + pl.id, + EXISTS(SELECT 1 FROM prefer_notice_acc pna WHERE pna.acc = pl.account) + OR EXISTS(SELECT 1 FROM prefer_notice pn WHERE pn.cloak = pl.hostmask), + EXISTS(SELECT 1 FROM simple_role_accs sra WHERE sra.acc = pl.account) + OR EXISTS(SELECT 1 FROM simple_role_notify srn WHERE srn.cloak = pl.hostmask), + EXISTS(SELECT 1 FROM deadchat_prefs dp + WHERE dp.user = COALESCE(pl.account, pl.hostmask) + AND dp.is_account = CASE WHEN pl.account IS NOT NULL THEN 1 ELSE 0 END), + pi.players, + COALESCE(sa.games, sh.games, 0), + CASE WHEN COALESCE(sa.games, sh.games) IS NOT NULL + THEN DATETIME('now', '+' || COALESCE(sa.games, sh.games) || ' hours') + ELSE NULL END +FROM player pl +LEFT JOIN pingif_prefs pi + ON pi.user = COALESCE(pl.account, pl.hostmask) + AND pi.is_account = CASE WHEN pl.account IS NOT NULL THEN 1 ELSE 0 END +LEFT JOIN stasised sh + ON sh.cloak = pl.hostmask +LEFT JOIN stasised_accs sa + ON sa.acc = pl.account; + +INSERT INTO person_player (person, player) +SELECT id, primary_player FROM person; + +-- Port allowed/denied stuff to the new format +-- (allowed to access entries, denied to warnings) +CREATE TEMPORARY TABLE access_flags_map ( + command TEXT NOT NULL, + flag TEXT NOT NULL +); +INSERT INTO access_flags_map +(command, flag) +VALUES +-- uppercase = dangerous to give out, lowercase = more ok to give out +-- F = full admin commands +('fallow', 'F'), +('fdeny', 'F'), +('fsend', 'F'), +-- s = speak commands +('fsay', 's'), +('fact', 's'), +-- d = debug commands +('fday', 'd'), +('fnight', 'd'), +('force', 'd'), +('rforce', 'd'), +('frole', 'd'), +('fgame', 'd'), +-- D = Dangerous commands +('fdie', 'D'), +('frestart', 'D'), +('fpull', 'D'), +('faftergame', 'D'), +('flastgame', 'D'), +-- A = administration commands +('fjoin', 'A'), +('fleave', 'A'), +('fstasis', 'A'), +('fstart', 'A'), +('fstop', 'A'), +('fwait', 'A'), +('fspectate', 'A'), +-- a = auspex commands +('revealroles', 'a'), +-- j = joke commands +('fgoat', 'j'), +-- m = management commands +('fsync', 'm'); + +INSERT INTO access (person, flags) +SELECT pe.id, GROUP_CONCAT(t.flag, '') +FROM ( + SELECT DISTINCT pl.id AS player, afm.flag AS flag + FROM allowed a + JOIN player pl + ON pl.hostmask = a.cloak + JOIN access_flags_map afm + ON afm.command = a.command + UNION + SELECT DISTINCT pl.id AS player, afm.flag AS flag + FROM allowed_accs a + JOIN player pl + ON pl.account = a.acc + JOIN access_flags_map afm + ON afm.command = a.command +) t +JOIN person pe + ON pe.primary_player = t.player +GROUP BY pe.id; + +INSERT INTO warning ( + target, + amount, + issued, + reason, + notes +) +SELECT + pe.id, + 0, + DATETIME('now'), + 'Unknown', + 'Automatically generated warning from migration' +FROM ( + SELECT DISTINCT pl.id AS player + FROM denied d + JOIN player pl + ON pl.hostmask = d.cloak + UNION + SELECT DISTINCT pl.id AS player + FROM denied_accs d + JOIN player pl + ON pl.account = d.acc +) t +JOIN person pe + ON pe.primary_player = t.player; + +INSERT INTO warning_sanction ( + warning, + sanction, + data +) +SELECT DISTINCT + w.id, + 'deny command', + COALESCE(dh.command, da.command) +FROM warning w +JOIN person pe + ON pe.id = w.target +JOIN player pl + ON pl.id = pe.primary_player +LEFT JOIN denied dh + ON dh.cloak = pl.hostmask +LEFT JOIN denied_accs da + ON da.acc = pl.account; + +DROP TABLE access_flags_map; + +-- Finally, clean up old tables +-- gamestats and rolestats are kept for posterity since that data is not migrated +-- pre_restart_state is kept because it is still used in the new schema +DROP TABLE allowed; +DROP TABLE allowed_accs; +DROP TABLE deadchat_prefs; +DROP TABLE denied; +DROP TABLE denied_accs; +DROP TABLE pingif_prefs; +DROP TABLE prefer_notice; +DROP TABLE prefer_notice_acc; +DROP TABLE roles; +DROP TABLE simple_role_accs; +DROP TABLE simple_role_notify; +DROP TABLE stasised; +DROP TABLE stasised_accs; diff --git a/src/settings.py b/src/settings.py index adc07d3..cdb0701 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,5 +1,4 @@ import fnmatch -import sqlite3 import re from collections import defaultdict, OrderedDict @@ -52,10 +51,6 @@ START_QUIT_DELAY = 10 MAX_PRIVMSG_TARGETS = 4 # how many mode values can be specified at once; used only as fallback MODELIMIT = 3 -LEAVE_STASIS_PENALTY = 1 -IDLE_STASIS_PENALTY = 1 -PART_STASIS_PENALTY = 1 -ACC_STASIS_PENALTY = 1 QUIET_DEAD_PLAYERS = False DEVOICE_DURING_NIGHT = False ALWAYS_PM_ROLE = False @@ -64,6 +59,32 @@ QUIET_PREFIX = "" # "" or "~q:" # The bot will automatically toggle those modes of people joining AUTO_TOGGLE_MODES = "" +LEAVE_PENALTY = 1 +LEAVE_EXPIRY = "30d" +IDLE_PENALTY = 1 +IDLE_EXPIRY = "30d" +PART_PENALTY = 1 +PART_EXPIRY = "30d" +ACC_PENALTY = 1 +ACC_EXPIRY = "30d" + +# 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) +# Valid sanctions are deny, stasis, scalestasis, and tempban +# 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}), + (15, 24, {"scalestasis": (0, 1, -10)}), + (25, 25, {"tempban": 15}) + ) + # The following is a bitfield, and they can be mixed together # Defaults to none of these, can be changed on a per-game-mode basis @@ -306,23 +327,20 @@ FORTUNE_CHANCE = 1/25 RULES = (botconfig.CHANNEL + " channel rules: http://wolf.xnrand.com/rules") -DENY = {} -ALLOW = {} - -DENY_ACCOUNTS = {} -ALLOW_ACCOUNTS = {} # pingif-related mappings PING_IF_PREFS = {} PING_IF_PREFS_ACCS = {} -PING_IF_NUMS = {} -PING_IF_NUMS_ACCS = {} +PING_IF_NUMS = defaultdict(set) +PING_IF_NUMS_ACCS = defaultdict(set) DEADCHAT_PREFS = set() DEADCHAT_PREFS_ACCS = set() +#TODO: move all of these to util.py or other files, as they are certainly NOT settings! + is_role = lambda plyr, rol: rol in ROLES and plyr in ROLES[rol] def match_hostmask(hostmask, nick, ident, host): @@ -336,43 +354,40 @@ def match_hostmask(hostmask, nick, ident, host): return False - -def check_priv(priv): - assert priv in ("owner", "admin") - - # Owners can do everything +def is_owner(nick, ident=None, host=None, acc=None): hosts = set(botconfig.OWNERS) accounts = set(botconfig.OWNERS_ACCOUNTS) + if nick in USERS: + if not ident: + ident = USERS[nick]["ident"] + if not host: + host = USERS[nick]["host"] + if not acc: + acc = USERS[nick]["account"] - if priv == "admin": - hosts.update(botconfig.ADMINS) - accounts.update(botconfig.ADMINS_ACCOUNTS) + if not DISABLE_ACCOUNTS and acc and acc != "*": + for pattern in accounts: + if fnmatch.fnmatch(acc.lower(), pattern.lower()): + return True - def do_check(nick, ident=None, host=None, acc=None): - if nick in USERS.keys(): - if not ident: - ident = USERS[nick]["ident"] - if not host: - host = USERS[nick]["host"] - if not acc: - acc = USERS[nick]["account"] + if host: + for hostmask in hosts: + if match_hostmask(hostmask, nick, ident, host): + return True - if not DISABLE_ACCOUNTS and acc and acc != "*": - for pattern in accounts: - if fnmatch.fnmatch(acc.lower(), pattern.lower()): - return True + return False - if host: - for hostmask in hosts: - if match_hostmask(hostmask, nick, ident, host): - return True - - return False - - return do_check - -is_admin = check_priv("admin") -is_owner = check_priv("owner") +def is_admin(nick, ident=None, host=None, acc=None): + if nick in USERS: + if not ident: + ident = USERS[nick]["ident"] + if not host: + host = USERS[nick]["host"] + if not acc: + acc = USERS[nick]["account"] + hostmask = nick + "!" + ident + "@" + host + flags = db.get_flags(acc, hostmask) + return "F" in flags def irc_lower(nick): mapping = { @@ -393,7 +408,6 @@ def irc_equals(nick1, nick2): return irc_lower(nick1) == irc_lower(nick2) def plural(role, count=2): - # TODO: use the "inflect" pip package, pass part-of-speech as a kwarg if count == 1: return role bits = role.split() @@ -408,6 +422,17 @@ def plural(role, count=2): "child": "children"}.get(bits[-1], bits[-1] + "s") return " ".join(bits) +def singular(plural): + # converse of plural above (kinda) + # this is used to map plural role names back to singular, + # so we don't need to worry about stuff like possessives + conv = {"wolves": "wolf", + "succubi": "succubus"} + if plural in conv: + return conv[plural] + # otherwise we just added an s on the end + return plural[:-1] + def list_players(roles = None): if roles is None: roles = ROLES.keys() @@ -501,362 +526,4 @@ def break_long_message(phrases, joinstr = " "): class InvalidModeException(Exception): pass -# Persistence - -conn = sqlite3.connect("data.sqlite3", check_same_thread = False) -c = conn.cursor() - -def init_db(): - with conn: - - c.execute('CREATE TABLE IF NOT EXISTS simple_role_notify (cloak TEXT)') # people who understand each role (hostmasks - backup) - - c.execute('CREATE TABLE IF NOT EXISTS simple_role_accs (acc TEXT)') # people who understand each role (accounts - primary) - - c.execute('CREATE TABLE IF NOT EXISTS prefer_notice (cloak TEXT)') # people who prefer /notice (hostmasks - backup) - - c.execute('CREATE TABLE IF NOT EXISTS prefer_notice_acc (acc TEXT)') # people who prefer /notice (accounts - primary) - - c.execute('CREATE TABLE IF NOT EXISTS stasised (cloak TEXT, games INTEGER, UNIQUE(cloak))') # stasised people (cloaks) - - c.execute('CREATE TABLE IF NOT EXISTS stasised_accs (acc TEXT, games INTEGER, UNIQUE(acc))') # stasised people (accounts - takes precedence) - - c.execute('CREATE TABLE IF NOT EXISTS denied (cloak TEXT, command TEXT, UNIQUE(cloak, command))') # DENY - - c.execute('CREATE TABLE IF NOT EXISTS denied_accs (acc TEXT, command TEXT, UNIQUE(acc, command))') # DENY_ACCOUNTS - - c.execute('CREATE TABLE IF NOT EXISTS allowed (cloak TEXT, command TEXT, UNIQUE(cloak, command))') # ALLOW - - c.execute('CREATE TABLE IF NOT EXISTS allowed_accs (acc TEXT, command TEXT, UNIQUE(acc, command))') # ALLOW_ACCOUNTS - - c.execute('CREATE TABLE IF NOT EXISTS pingif_prefs (user TEXT, is_account BOOLEAN, players INTEGER, PRIMARY KEY(user, is_account))') # pingif player count preferences - c.execute('CREATE INDEX IF NOT EXISTS ix_ping_prefs_pingif ON pingif_prefs (players ASC)') # index apparently makes it faster - - c.execute('CREATE TABLE IF NOT EXISTS deadchat_prefs (user TEXT, is_account BOOLEAN)') # deadcht preferences - - c.execute('PRAGMA table_info(pre_restart_state)') - try: - next(c) - except StopIteration: - c.execute('CREATE TABLE pre_restart_state (players TEXT)') - c.execute('INSERT INTO pre_restart_state (players) VALUES (NULL)') - - c.execute('SELECT * FROM simple_role_notify') - for row in c: - SIMPLE_NOTIFY.add(row[0]) - - c.execute('SELECT * FROM simple_role_accs') - for row in c: - SIMPLE_NOTIFY_ACCS.add(row[0]) - - c.execute('SELECT * FROM prefer_notice') - for row in c: - PREFER_NOTICE.add(row[0]) - - c.execute('SELECT * FROM prefer_notice_acc') - for row in c: - PREFER_NOTICE_ACCS.add(row[0]) - - c.execute('SELECT * FROM stasised') - for row in c: - STASISED[row[0]] = row[1] - - c.execute('SELECT * FROM stasised_accs') - for row in c: - STASISED_ACCS[row[0]] = row[1] - - c.execute('SELECT * FROM denied') - for row in c: - if row[0] not in DENY: - DENY[row[0]] = set() - DENY[row[0]].add(row[1]) - - c.execute('SELECT * FROM denied_accs') - for row in c: - if row[0] not in DENY_ACCOUNTS: - DENY_ACCOUNTS[row[0]] = set() - DENY_ACCOUNTS[row[0]].add(row[1]) - - c.execute('SELECT * FROM allowed') - for row in c: - if row[0] not in ALLOW: - ALLOW[row[0]] = set() - ALLOW[row[0]].add(row[1]) - - c.execute('SELECT * FROM allowed_accs') - for row in c: - if row[0] not in ALLOW_ACCOUNTS: - ALLOW_ACCOUNTS[row[0]] = set() - ALLOW_ACCOUNTS[row[0]].add(row[1]) - - c.execute('SELECT * FROM pingif_prefs') - for row in c: - # is an account - if row[1]: - if row[0] not in PING_IF_PREFS_ACCS: - PING_IF_PREFS_ACCS[row[0]] = row[2] - if row[2] not in PING_IF_NUMS_ACCS: - PING_IF_NUMS_ACCS[row[2]] = set() - PING_IF_NUMS_ACCS[row[2]].add(row[0]) - # is a host - else: - if row[0] not in PING_IF_PREFS: - PING_IF_PREFS[row[0]] = row[2] - if row[2] not in PING_IF_NUMS: - PING_IF_NUMS[row[2]] = set() - PING_IF_NUMS[row[2]].add(row[0]) - - c.execute('SELECT * FROM deadchat_prefs') - for user, is_acc in c: - if is_acc: - DEADCHAT_PREFS_ACCS.add(user) - else: - DEADCHAT_PREFS.add(user) - - # populate the roles table - c.execute('DROP TABLE IF EXISTS roles') - c.execute('CREATE TABLE roles (id INTEGER PRIMARY KEY AUTOINCREMENT, role TEXT)') - - for x in list(ROLE_GUIDE): - c.execute("INSERT OR REPLACE INTO roles (role) VALUES (?)", (x,)) - - - c.execute(('CREATE TABLE IF NOT EXISTS rolestats (player TEXT, role TEXT, '+ - 'teamwins SMALLINT, individualwins SMALLINT, totalgames SMALLINT, '+ - 'UNIQUE(player, role))')) - - - c.execute(('CREATE TABLE IF NOT EXISTS gamestats (gamemode TEXT, size SMALLINT, villagewins SMALLINT, ' + - 'wolfwins SMALLINT, monsterwins SMALLINT, foolwins SMALLINT, piperwins SMALLINT, succubuswins SMALLINT, ' + - 'demoniacwins SMALLINT, totalgames SMALLINT, UNIQUE(gamemode, size))')) - try: - # Check if table has been updated with new stats - c.execute('SELECT succubuswins from gamestats') - for row in c: - # Read all the very important data - pass - except sqlite3.OperationalError: - c.execute('ALTER TABLE gamestats RENAME TO gamestatsold') - c.execute('CREATE TABLE gamestats (gamemode TEXT, size SMALLINT, villagewins SMALLINT, wolfwins SMALLINT, ' + - 'monsterwins SMALLINT, foolwins SMALLINT, piperwins SMALLINT,succubuswins SMALLINT, ' + - 'demoniacwins SMALLINT, totalgames SMALLINT, UNIQUE(gamemode, size))') - c.execute('INSERT into gamestats (gamemode, size, villagewins, wolfwins, monsterwins, foolwins, piperwins, succubuswins, demoniacwins, totalgames) ' + - 'SELECT gamemode, size, villagewins, wolfwins, monsterwins, foolwins, piperwins, 0, 0, totalgames FROM gamestatsold') - c.execute('DROP TABLE gamestatsold') - - -def remove_simple_rolemsg(clk): - with conn: - c.execute('DELETE from simple_role_notify where cloak=?', (clk,)) - -def add_simple_rolemsg(clk): - with conn: - c.execute('INSERT into simple_role_notify VALUES (?)', (clk,)) - -def remove_simple_rolemsg_acc(acc): - with conn: - c.execute('DELETE from simple_role_accs where acc=?', (acc,)) - -def add_simple_rolemsg_acc(acc): - with conn: - c.execute('INSERT into simple_role_accs VALUES (?)', (acc,)) - -def remove_prefer_notice(clk): - with conn: - c.execute('DELETE from prefer_notice where cloak=?', (clk,)) - -def add_prefer_notice(clk): - with conn: - c.execute('INSERT into prefer_notice VALUES (?)', (clk,)) - -def remove_prefer_notice_acc(acc): - with conn: - c.execute('DELETE from prefer_notice_acc where acc=?', (acc,)) - -def add_prefer_notice_acc(acc): - with conn: - c.execute('INSERT into prefer_notice_acc VALUES (?)', (acc,)) - -def set_stasis(clk, games): - with conn: - if games <= 0: - c.execute('DELETE FROM stasised WHERE cloak=?', (clk,)) - else: - c.execute('INSERT OR REPLACE INTO stasised VALUES (?,?)', (clk, games)) - -def set_stasis_acc(acc, games): - with conn: - if games <= 0: - c.execute('DELETE FROM stasised_accs WHERE acc=?', (acc,)) - else: - c.execute('INSERT OR REPLACE INTO stasised_accs VALUES (?,?)', (acc, games)) - -def add_deny(clk, command): - with conn: - c.execute('INSERT OR IGNORE INTO denied VALUES (?,?)', (clk, command)) - -def remove_deny(clk, command): - with conn: - c.execute('DELETE FROM denied WHERE cloak=? AND command=?', (clk, command)) - -def add_deny_acc(acc, command): - with conn: - c.execute('INSERT OR IGNORE INTO denied_accs VALUES (?,?)', (acc, command)) - -def remove_deny_acc(acc, command): - with conn: - c.execute('DELETE FROM denied_accs WHERE acc=? AND command=?', (acc, command)) - -def add_allow(clk, command): - with conn: - c.execute('INSERT OR IGNORE INTO allowed VALUES (?,?)', (clk, command)) - -def remove_allow(clk, command): - with conn: - c.execute('DELETE FROM allowed WHERE cloak=? AND command=?', (clk, command)) - -def add_allow_acc(acc, command): - with conn: - c.execute('INSERT OR IGNORE INTO allowed_accs VALUES (?,?)', (acc, command)) - -def remove_allow_acc(acc, command): - with conn: - c.execute('DELETE FROM allowed_accs WHERE acc=? AND command=?', (acc, command)) - -def set_pingif_status(user, is_account, players): - with conn: - c.execute('DELETE FROM pingif_prefs WHERE user=? AND is_account=?', (user, is_account)) - if players != 0: - c.execute('INSERT OR REPLACE INTO pingif_prefs VALUES (?,?,?)', (user, is_account, players)) - -def add_deadchat_pref(user, is_account): - with conn: - c.execute('INSERT OR REPLACE INTO deadchat_prefs VALUES (?,?)', (user, is_account)) - -def remove_deadchat_pref(user, is_account): - with conn: - c.execute('DELETE FROM deadchat_prefs WHERE user=? AND is_account=?', (user, is_account)) - -def update_role_stats(acc, role, won, iwon): - with conn: - wins, iwins, total = 0, 0, 0 - - c.execute(("SELECT teamwins, individualwins, totalgames FROM rolestats "+ - "WHERE player=? AND role=?"), (acc, role)) - row = c.fetchone() - if row: - wins, iwins, total = row - - if won: - wins += 1 - if iwon: - iwins += 1 - total += 1 - - c.execute("INSERT OR REPLACE INTO rolestats VALUES (?,?,?,?,?)", - (acc, role, wins, iwins, total)) - -def update_game_stats(gamemode, size, winner): - with conn: - vwins, wwins, mwins, fwins, pwins, swins, dwins, total = 0, 0, 0, 0, 0, 0, 0, 0 - - c.execute("SELECT villagewins, wolfwins, monsterwins, foolwins, piperwins, succubuswins, " - "demoniacwins, totalgames FROM gamestats WHERE gamemode=? AND size=?", (gamemode, size)) - row = c.fetchone() - if row: - vwins, wwins, mwins, fwins, pwins, swins, dwins, total = row - - if winner == "wolves": - wwins += 1 - elif winner == "villagers": - vwins += 1 - elif winner == "monsters": - mwins += 1 - elif winner == "pipers": - pwins += 1 - elif winner == "succubi": - swins += 1 - elif winner == "demoniacs": - dwins += 1 - elif winner.startswith("@"): - fwins += 1 - total += 1 - - c.execute("INSERT OR REPLACE INTO gamestats VALUES (?,?,?,?,?,?,?,?,?,?)", - (gamemode, size, vwins, wwins, mwins, fwins, pwins, swins, dwins, total)) - -def get_player_stats(acc, role): - if role.lower() not in [k.lower() for k in ROLE_GUIDE.keys()] and role != "lover": - return "No such role: {0}".format(role) - with conn: - c.execute("SELECT player FROM rolestats WHERE player=? COLLATE NOCASE", (acc,)) - player = c.fetchone() - if player: - for row in c.execute("SELECT * FROM rolestats WHERE player=? COLLATE NOCASE AND role=? COLLATE NOCASE", (acc, role)): - msg = "\u0002{0}\u0002 as \u0002{1}\u0002 | Team wins: {2} (%d%%), Individual wins: {3} (%d%%), Total games: {4}".format(*row) - return msg % (round(row[2]/row[4] * 100), round(row[3]/row[4] * 100)) - else: - return "No stats for {0} as {1}.".format(player[0], role) - return "{0} has not played any games.".format(acc) - -def get_player_totals(acc): - role_totals = [] - with conn: - c.execute("SELECT player FROM rolestats WHERE player=? COLLATE NOCASE", (acc,)) - player = c.fetchone() - if player: - c.execute("SELECT role, totalgames FROM rolestats WHERE player=? COLLATE NOCASE ORDER BY totalgames DESC", (acc,)) - role_tmp = defaultdict(int) - totalgames = 0 - while True: - row = c.fetchone() - if row: - role_tmp[row[0]] += row[1] - if row[0] not in TEMPLATE_RESTRICTIONS and row[0] != "lover": - totalgames += row[1] - else: - break - order = role_order() - #ordered role stats - role_totals = ["\u0002{0}\u0002: {1}".format(role, role_tmp[role]) for role in order if role in role_tmp] - #lover or any other special stats - role_totals += ["\u0002{0}\u0002: {1}".format(role, count) for role, count in role_tmp.items() if role not in order] - return "\u0002{0}\u0002's totals | \u0002{1}\u0002 games | {2}".format(player[0], totalgames, break_long_message(role_totals, ", ")) - else: - return "\u0002{0}\u0002 has not played any games.".format(acc) - -def get_game_stats(gamemode, size): - with conn: - for row in c.execute("SELECT * FROM gamestats WHERE gamemode=? AND size=?", (gamemode, size)): - msg = "\u0002%d\u0002 player games | Village wins: %d (%d%%), Wolf wins: %d (%d%%)" % (row[1], row[2], round(row[2]/row[9] * 100), row[3], round(row[3]/row[9] * 100)) - if row[4] > 0: - msg += ", Monster wins: %d (%d%%)" % (row[4], round(row[4]/row[9] * 100)) - if row[5] > 0: - msg += ", Fool wins: %d (%d%%)" % (row[5], round(row[5]/row[9] * 100)) - if row[6] > 0: - msg += ", Piper wins: %d (%d%%)" % (row[6], round(row[6]/row[9] * 100)) - if row[7] > 0: - msg += ", Succubus wins: %d (%d%%)" % (row[7], round(row[7]/row[9] * 100)) - if row[8] > 0: - msg += ", Demoniac wins: %d (%d%%)" % (row[8], round(row[8]/row[9] * 100)) - return msg + ", Total games: {0}".format(row[9]) - else: - return "No stats for \u0002{0}\u0002 player games.".format(size) - -def get_game_totals(gamemode): - size_totals = [] - total = 0 - with conn: - for size in range(MIN_PLAYERS, MAX_PLAYERS + 1): - c.execute("SELECT size, totalgames FROM gamestats WHERE gamemode=? AND size=?", (gamemode, size)) - row = c.fetchone() - if row: - size_totals.append("\u0002{0}p\u0002: {1}".format(*row)) - total += row[1] - - if len(size_totals) == 0: - return "No games have been played in the {0} game mode.".format(gamemode) - else: - return "Total games ({0}) | {1}".format(total, ", ".join(size_totals)) - # vim: set sw=4 expandtab: diff --git a/src/wolfgame.py b/src/wolfgame.py index 37589cb..5bbf539 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -45,7 +45,7 @@ from oyoyo.parse import parse_nick import botconfig import src.settings as var from src.utilities import * -from src import decorators, events, logger, proxy, debuglog, errlog, plog +from src import db, decorators, events, logger, proxy, debuglog, errlog, plog from src.messages import messages # done this way so that events is accessible in !eval (useful for debugging) @@ -429,36 +429,7 @@ def reset(): reset() -def make_stasis(nick, penalty): - if nick in var.USERS: - ident = var.USERS[nick]["ident"] - host = var.USERS[nick]["host"] - acc = var.USERS[nick]["account"] - else: - return # Can't do it - if not acc or acc == "*": - acc = None - if not host and not acc: - return # Can't do it, either - if acc: - if penalty == 0: - if acc in var.STASISED_ACCS: - del var.STASISED_ACCS[acc] - var.set_stasis_acc(acc, 0) - else: - var.STASISED_ACCS[acc] += penalty - var.set_stasis_acc(acc, var.STASISED_ACCS[acc]) - if (not var.ACCOUNTS_ONLY or not acc) and host: - hostmask = ident + "@" + host - if penalty == 0: - if hostmask in var.STASISED: - del var.STASISED[hostmask] - var.set_stasis(hostmask, 0) - else: - var.STASISED[hostmask] += penalty - var.set_stasis(hostmask, var.STASISED[hostmask]) - -@cmd("fsync", admin_only=True, pm=True) +@cmd("fsync", flag="m", pm=True) def fsync(cli, nick, chan, rest): """Makes the bot apply the currently appropriate channel modes.""" sync_modes(cli) @@ -479,7 +450,7 @@ def sync_modes(cli): mass_mode(cli, voices, other) -@cmd("fdie", "fbye", admin_only=True, pm=True) +@cmd("fdie", "fbye", flag="D", pm=True) def forced_exit(cli, nick, chan, rest): """Forces the bot to close.""" @@ -540,7 +511,7 @@ def _restart_program(cli, mode=None): os.execl(python, python, *sys.argv) -@cmd("frestart", admin_only=True, pm=True) +@cmd("frestart", flag="D", pm=True) def restart_program(cli, nick, chan, rest): """Restarts the bot.""" @@ -657,20 +628,20 @@ def mark_simple_notify(cli, nick, chan, rest): if acc: # Prioritize account if acc in var.SIMPLE_NOTIFY_ACCS: var.SIMPLE_NOTIFY_ACCS.remove(acc) - var.remove_simple_rolemsg_acc(acc) + db.toggle_simple(acc, None) if host in var.SIMPLE_NOTIFY: var.SIMPLE_NOTIFY.remove(host) - var.remove_simple_rolemsg(host) + db.toggle_simple(None, host) fullmask = ident + "@" + host if fullmask in var.SIMPLE_NOTIFY: var.SIMPLE_NOTIFY.remove(fullmask) - var.remove_simple_rolemsg(fullmask) + db.toggle_simple(None, fullmask) cli.notice(nick, messages["simple_off"]) return var.SIMPLE_NOTIFY_ACCS.add(acc) - var.add_simple_rolemsg_acc(acc) + db.toggle_simple(acc, None) elif var.ACCOUNTS_ONLY: cli.notice(nick, messages["not_logged_in"]) return @@ -678,7 +649,7 @@ def mark_simple_notify(cli, nick, chan, rest): else: # Not logged in, fall back to ident@hostmask if host in var.SIMPLE_NOTIFY: var.SIMPLE_NOTIFY.remove(host) - var.remove_simple_rolemsg(host) + db.toggle_simple(None, host) cli.notice(nick, messages["simple_off"]) return @@ -686,13 +657,13 @@ def mark_simple_notify(cli, nick, chan, rest): fullmask = ident + "@" + host if fullmask in var.SIMPLE_NOTIFY: var.SIMPLE_NOTIFY.remove(fullmask) - var.remove_simple_rolemsg(fullmask) + db.toggle_simple(None, fullmask) cli.notice(nick, messages["simple_off"]) return var.SIMPLE_NOTIFY.add(fullmask) - var.add_simple_rolemsg(fullmask) + db.toggle_simple(None, fullmask) cli.notice(nick, messages["simple_on"]) @@ -713,20 +684,20 @@ def mark_prefer_notice(cli, nick, chan, rest): if acc and not var.DISABLE_ACCOUNTS: # Do things by account if logged in if acc in var.PREFER_NOTICE_ACCS: var.PREFER_NOTICE_ACCS.remove(acc) - var.remove_prefer_notice_acc(acc) + db.toggle_notice(acc, None) if host in var.PREFER_NOTICE: var.PREFER_NOTICE.remove(host) - var.remove_prefer_notice(host) + db.toggle_notice(None, host) fullmask = ident + "@" + host if fullmask in var.PREFER_NOTICE: var.PREFER_NOTICE.remove(fullmask) - var.remove_prefer_notice(fullmask) + db.toggle_notice(None, fullmask) cli.notice(nick, messages["notice_off"]) return var.PREFER_NOTICE_ACCS.add(acc) - var.add_prefer_notice_acc(acc) + db.toggle_notice(acc, None) elif var.ACCOUNTS_ONLY: cli.notice(nick, messages["not_logged_in"]) return @@ -734,20 +705,20 @@ def mark_prefer_notice(cli, nick, chan, rest): else: # Not logged in if host in var.PREFER_NOTICE: var.PREFER_NOTICE.remove(host) - var.remove_prefer_notice(host) + db.toggle_notice(None, host) cli.notice(nick, messages["notice_off"]) return fullmask = ident + "@" + host if fullmask in var.PREFER_NOTICE: var.PREFER_NOTICE.remove(fullmask) - var.remove_prefer_notice(fullmask) + db.toggle_notice(None, fullmask) cli.notice(nick, messages["notice_off"]) return var.PREFER_NOTICE.add(fullmask) - var.add_prefer_notice(fullmask) + db.toggle_notice(None, fullmask) cli.notice(nick, messages["notice_on"]) @@ -914,7 +885,7 @@ def toggle_altpinged_status(nick, value, old=None): if not var.DISABLE_ACCOUNTS and acc and acc != "*": if acc in var.PING_IF_PREFS_ACCS: del var.PING_IF_PREFS_ACCS[acc] - var.set_pingif_status(acc, True, 0) + db.set_pingif(0, acc, None) if old is not None: with var.WARNING_LOCK: if old in var.PING_IF_NUMS_ACCS: @@ -923,7 +894,7 @@ def toggle_altpinged_status(nick, value, old=None): for hostmask in list(var.PING_IF_PREFS.keys()): if var.match_hostmask(hostmask, nick, ident, host): del var.PING_IF_PREFS[hostmask] - var.set_pingif_status(hostmask, False, 0) + db.set_pingif(0, None, hostmask) if old is not None: with var.WARNING_LOCK: if old in var.PING_IF_NUMS.keys(): @@ -932,7 +903,7 @@ def toggle_altpinged_status(nick, value, old=None): else: if not var.DISABLE_ACCOUNTS and acc and acc != "*": var.PING_IF_PREFS_ACCS[acc] = value - var.set_pingif_status(acc, True, value) + db.set_pingif(value, acc, None) with var.WARNING_LOCK: if value not in var.PING_IF_NUMS_ACCS: var.PING_IF_NUMS_ACCS[value] = set() @@ -943,7 +914,7 @@ def toggle_altpinged_status(nick, value, old=None): elif not var.ACCOUNTS_ONLY: hostmask = ident + "@" + host var.PING_IF_PREFS[hostmask] = value - var.set_pingif_status(hostmask, False, value) + db.set_pingif(value, None, hostmask) with var.WARNING_LOCK: if value not in var.PING_IF_NUMS.keys(): var.PING_IF_NUMS[value] = set() @@ -1139,12 +1110,12 @@ def deadchat_pref(cli, nick, chan, rest): if value in variable: msg = messages["chat_on_death"] variable.remove(value) - var.remove_deadchat_pref(value, value == acc) + db.toggle_deadchat(acc, host) else: msg = messages["no_chat_on_death"] variable.add(value) - var.add_deadchat_pref(value, value == acc) + db.toggle_deadchat(acc, host) reply(cli, nick, chan, msg, private=True) @@ -1205,13 +1176,7 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): if stasis > 0: if forced and stasis == 1: - for hostmask in list(var.STASISED.keys()): - if var.match_hostmask(hostmask, player, ident, host): - var.set_stasis(hostmask, 0) - del var.STASISED[hostmask] - if not var.DISABLE_ACCOUNTS and acc in var.STASISED_ACCS: - var.set_stasis_acc(acc, 0) - del var.STASISED_ACCS[acc] + decrement_stasis(player) else: cli.notice(who, messages["stasis"].format( "you are" if player == who else player + " is", stasis, @@ -1337,7 +1302,7 @@ def kill_join(cli, chan): var.AFTER_FLASTGAME = None -@cmd("fjoin", admin_only=True) +@cmd("fjoin", flag="A") def fjoin(cli, nick, chan, rest): """Forces someone to join a game.""" # keep this and the event in def join() in sync @@ -1395,7 +1360,7 @@ def fjoin(cli, nick, chan, rest): if fake: cli.msg(chan, messages["fjoin_success"].format(nick, len(var.list_players()))) -@cmd("fleave", "fquit", admin_only=True, pm=True, phases=("join", "day", "night")) +@cmd("fleave", "fquit", flag="A", pm=True, phases=("join", "day", "night")) def fleave(cli, nick, chan, rest): """Forces someone to leave the game.""" @@ -1428,7 +1393,7 @@ def fleave(cli, nick, chan, rest): if a in rset: var.ORIGINAL_ROLES[r].remove(a) var.ORIGINAL_ROLES[r].add("(dced)"+a) - make_stasis(a, var.LEAVE_STASIS_PENALTY) + add_warning(a, var.LEAVE_PENALTY, botconfig.NICK, messages["leave_warning"]) if a in var.PLAYERS: var.DCED_PLAYERS[a] = var.PLAYERS.pop(a) @@ -1445,7 +1410,7 @@ def fleave(cli, nick, chan, rest): cli.msg(chan, messages["not_playing"].format(a)) return -@cmd("fstart", admin_only=True, phases=("join",)) +@cmd("fstart", flag="A", phases=("join",)) def fstart(cli, nick, chan, rest): """Forces the game to start immediately.""" cli.msg(botconfig.CHANNEL, messages["fstart_success"].format(nick)) @@ -2129,7 +2094,7 @@ def hurry_up(cli, gameid, change): -@cmd("fnight", admin_only=True) +@cmd("fnight", flag="d") def fnight(cli, nick, chan, rest): """Forces the day to end and night to begin.""" if var.PHASE != "day": @@ -2138,7 +2103,7 @@ def fnight(cli, nick, chan, rest): hurry_up(cli, 0, True) -@cmd("fday", admin_only=True) +@cmd("fday", flag="d") def fday(cli, nick, chan, rest): """Forces the night to end and the next day to begin.""" if var.PHASE != "night": @@ -2536,14 +2501,19 @@ def stop_game(cli, winner = "", abort = False, additional_winners = None): # Only update if someone actually won, "" indicates everyone died or abnormal game stop if winner != "": plrl = {} + pltp = defaultdict(list) winners = [] + player_list = [] if additional_winners is not None: winners.extend(additional_winners) for role,ppl in var.ORIGINAL_ROLES.items(): if role in var.TEMPLATE_RESTRICTIONS.keys(): + for x in ppl: + if x is not None: + pltp[x].append(role) continue for x in ppl: - if x != None: + if x is not None: if x in var.FINAL_ROLES: plrl[x] = var.FINAL_ROLES[x] else: @@ -2551,24 +2521,42 @@ def stop_game(cli, winner = "", abort = False, additional_winners = None): for plr, rol in plrl.items(): orol = rol # original role, since we overwrite rol in case of clone splr = plr # plr stripped of the (dced) bit at the front, since other dicts don't have that - # TODO: figure out how player stats should work when var.DISABLE_ACCOUNTS is True; likely track by nick + pentry = {"nick": None, + "account": None, + "ident": None, + "host": None, + "role": None, + "templates": [], + "special": [], + "won": False, + "iwon": False, + "dced": False} if plr.startswith("(dced)"): + pentry["dced"] = True splr = plr[6:] - if var.DISABLE_ACCOUNTS: - acc = splr - elif splr in var.DCED_PLAYERS.keys(): - acc = var.DCED_PLAYERS[splr]["account"] - elif splr in var.PLAYERS.keys(): - acc = var.PLAYERS[splr]["account"] - else: - acc = "*" - elif plr in var.PLAYERS.keys(): - if var.DISABLE_ACCOUNTS: - acc = plr - else: - acc = var.PLAYERS[plr]["account"] - else: - acc = "*" #probably fjoin'd fake + if splr in var.USERS: + if not var.DISABLE_ACCOUNTS: + pentry["account"] = var.USERS[splr]["account"] + pentry["nick"] = splr + pentry["ident"] = var.USERS[splr]["ident"] + pentry["host"] = var.USERS[splr]["host"] + elif plr in var.USERS: + if not var.DISABLE_ACCOUNTS: + pentry["account"] = var.USERS[plr]["account"] + pentry["nick"] = plr + pentry["ident"] = var.USERS[plr]["ident"] + pentry["host"] = var.USERS[plr]["host"] + + pentry["role"] = rol + pentry["templates"] = pltp[plr] + if splr in var.LOVERS: + pentry["special"].append("lover") + if splr in var.ENTRANCED: + pentry["special"].append("entranced") + if splr in var.VENGEFUL_GHOSTS: + pentry["special"].append("vg activated") + if var.VENGEFUL_GHOSTS[splr][0] == "!": + pentry["special"].append("vg driven off") won = False iwon = False @@ -2668,18 +2656,31 @@ def stop_game(cli, winner = "", abort = False, additional_winners = None): elif not iwon: iwon = won and splr in survived # survived, team won = individual win - if acc != "*": - var.update_role_stats(acc, orol, won, iwon) - for role in var.TEMPLATE_RESTRICTIONS.keys(): - if plr in var.ORIGINAL_ROLES[role]: - var.update_role_stats(acc, role, won, iwon) - if splr in var.LOVERS: - var.update_role_stats(acc, "lover", won, iwon) + pentry["won"] = won + pentry["iwon"] = iwon if won or iwon: winners.append(splr) - var.update_game_stats(var.CURRENT_GAMEMODE.name, len(survived) + len(var.DEAD), winner) + if pentry["nick"] is not None: + # don't record fjoined fakes + player_list.append(pentry) + + game_options = {"role reveal": var.ROLE_REVEAL, + "stats": var.STATS_TYPE, + "abstain": "on" if var.ABSTAIN_ENABLED and not var.LIMIT_ABSTAIN else "restricted" if var.ABSTAIN_ENABLED else "off", + "roles": {}} + for role,pl in var.ORIGINAL_ROLES.items(): + if len(pl) > 0: + game_options["roles"][role] = len(pl) + + db.add_game(var.CURRENT_GAMEMODE.name, + len(survived) + len(var.DEAD), + time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(var.GAME_ID)), + time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()), + winner, + player_list, + game_options) # spit out the list of winners winners.sort() @@ -3379,7 +3380,7 @@ def reaper(cli, gameid): if nck in rlist: var.ORIGINAL_ROLES[r].remove(nck) var.ORIGINAL_ROLES[r].add("(dced)"+nck) - make_stasis(nck, var.IDLE_STASIS_PENALTY) + add_warning(nck, var.IDLE_PENALTY, botconfig.NICK, messages["idle_warning"]) del_player(cli, nck, end_game = False, death_triggers = False) chk_win(cli) pl = var.list_players() @@ -3396,7 +3397,7 @@ def reaper(cli, gameid): else: cli.msg(chan, messages["quit_death_no_reveal"].format(dcedplayer)) if var.PHASE != "join": - make_stasis(dcedplayer, var.PART_STASIS_PENALTY) + add_warning(dcedplayer, var.PART_PENALTY, botconfig.NICK, messages["part_warning"]) 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): @@ -3405,7 +3406,7 @@ def reaper(cli, gameid): else: cli.msg(chan, messages["part_death_no_reveal"].format(dcedplayer)) if var.PHASE != "join": - make_stasis(dcedplayer, var.PART_STASIS_PENALTY) + add_warning(dcedplayer, var.PART_PENALTY, botconfig.NICK, messages["part_warning"]) 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): @@ -3414,7 +3415,7 @@ def reaper(cli, gameid): else: cli.msg(chan, messages["account_death_no_reveal"].format(dcedplayer)) if var.PHASE != "join": - make_stasis(dcedplayer, var.ACC_STASIS_PENALTY) + add_warning(dcedplayer, var.ACC_PENALTY, botconfig.NICK, messages["acc_warning"]) if not del_player(cli, dcedplayer, devoice = False, death_triggers = False): return time.sleep(10) @@ -3514,7 +3515,7 @@ def goat(cli, nick, chan, rest): var.GOATED = True -@cmd("fgoat", admin_only=True) +@cmd("fgoat", flag="j") def fgoat(cli, nick, chan, rest): """Forces a goat to interact with anyone or anything, without limitations.""" nick_ = rest.split(' ')[0].strip() @@ -3842,7 +3843,7 @@ def leave(cli, what, nick, why=""): msg = (messages["leave_death"] + "{2}").format(nick, var.get_reveal_role(nick), population) else: msg = (messages["leave_death_no_reveal"] + "{1}").format(nick, population) - make_stasis(nick, var.LEAVE_STASIS_PENALTY) + add_warning(nick, var.LEAVE_PENALTY, botconfig.NICK, messages["leave_warning"]) cli.msg(botconfig.CHANNEL, msg) var.SPECTATING_WOLFCHAT.discard(nick) var.SPECTATING_DEADCHAT.discard(nick) @@ -3907,7 +3908,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) - make_stasis(nick, var.LEAVE_STASIS_PENALTY) + add_warning(nick, var.LEAVE_PENALTY, botconfig.NICK, messages["leave_warning"]) if nick in var.PLAYERS: var.DCED_PLAYERS[nick] = var.PLAYERS.pop(nick) @@ -7866,18 +7867,7 @@ def start(cli, nick, chan, forced = False, restart = ""): var.GAMEPHASE = "day" transition_day(cli) - for hostmask in list(var.STASISED.keys()): - var.STASISED[hostmask] -= 1 - var.set_stasis(hostmask, var.STASISED[hostmask]) - if var.STASISED[hostmask] <= 0: - del var.STASISED[hostmask] - - if not var.DISABLE_ACCOUNTS: - for acc in list(var.STASISED_ACCS.keys()): - var.STASISED_ACCS[acc] -= 1 - var.set_stasis_acc(acc, var.STASISED_ACCS[acc]) - if var.STASISED_ACCS[acc] <= 0: - del var.STASISED_ACCS[acc] + decrement_stasis() if not botconfig.DEBUG_MODE or not var.DISABLE_DEBUG_MODE_REAPER: # DEATH TO IDLERS! @@ -7885,8 +7875,6 @@ def start(cli, nick, chan, forced = False, restart = ""): reapertimer.daemon = True reapertimer.start() - - @hook("error") def on_error(cli, pfx, msg): if var.RESTARTING or msg.endswith("(Excess Flood)"): @@ -7894,144 +7882,6 @@ def on_error(cli, pfx, msg): elif msg.startswith("Closing Link:"): raise SystemExit -@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", admin_only=True, pm=True) -def fstasis(cli, nick, chan, rest): - """Removes or sets stasis penalties.""" - - data = rest.split() - msg = None - - if data: - lusers = {k.lower(): v for k, v in var.USERS.items()} - user = data[0] - - if user.lower() in lusers: - ident = lusers[user.lower()]["ident"] - host = lusers[user.lower()]["host"] - acc = lusers[user.lower()]["account"] - hostmask = ident + "@" + host - else: - hostmask = user - acc = None - if var.ACCOUNTS_ONLY and acc == "*": - acc = None - hostmask = None - msg = messages["account_not_logged_in"].format(user) - if not acc and user in var.STASISED_ACCS: - acc = user - - err_msg = messages["stasis_non_negative"] - if (not var.ACCOUNTS_ONLY or not acc) and hostmask: - if len(data) == 1: - if hostmask in var.STASISED: - plural = "" if var.STASISED[hostmask] == 1 else "s" - msg = messages["hostmask_in_stasis"].format(data[0], hostmask, var.STASISED[hostmask], plural) - else: - msg = messages["hostmask_not_in_stasis"].format(data[0], hostmask) - else: - try: - amt = int(data[1]) - except ValueError: - if chan == nick: - pm(cli, nick, err_msg) - else: - cli.notice(nick, err_msg) - - return - - if amt < 0: - if chan == nick: - pm(cli, nick, err_msg) - else: - cli.notice(nick, err_msg) - - return - elif amt > 2**31-1: - amt = 2**31-1 - - if amt > 0: - var.STASISED[hostmask] = amt - var.set_stasis(hostmask, amt) - plural = "" if amt == 1 else "s" - msg = messages["fstasis_hostmask_add"].format(data[0], hostmask, amt, plural) - elif amt == 0: - if hostmask in var.STASISED: - del var.STASISED[hostmask] - var.set_stasis(hostmask, 0) - msg = messages["fstasis_hostmask_remove"].format(data[0], hostmask) - else: - msg = messages["hostmask_not_in_stasis"].format(data[0], hostmask) - if not var.DISABLE_ACCOUNTS and acc: - if len(data) == 1: - if acc in var.STASISED_ACCS: - plural = "" if var.STASISED_ACCS[acc] == 1 else "s" - msg = messages["account_in_stasis"].format(data[0], acc, var.STASISED_ACCS[acc], plural) - else: - msg = messages["account_not_in_stasis"].format(data[0], acc) - else: - try: - amt = int(data[1]) - except ValueError: - if chan == nick: - pm(cli, nick, err_msg) - else: - cli.notice(nick, err_msg) - return - - if amt < 0: - if chan == nick: - pm(cli, nick, err_msg) - else: - cli.notice(nick, err_msg) - return - elif amt > 2**31-1: - amt = 2**31-1 - - if amt > 0: - var.STASISED_ACCS[acc] = amt - var.set_stasis_acc(acc, amt) - plural = "" if amt == 1 else "s" - msg = messages["fstasis_account_add"].format(data[0], acc, amt, plural) - elif amt == 0: - if acc in var.STASISED_ACCS: - del var.STASISED_ACCS[acc] - var.set_stasis_acc(acc, 0) - msg = messages["fstasis_account_remove"].format(data[0], acc) - else: - msg = messages["account_not_in_stasis"].format(data[0], acc) - 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())) - else: - msg = messages["noone_stasised"] - - if msg: - if chan == nick: - pm(cli, nick, msg) - else: - cli.msg(chan, msg) - def is_user_stasised(nick): """Checks if a user is in stasis. Returns a number of games in stasis.""" @@ -8050,272 +7900,327 @@ def is_user_stasised(nick): amount = max(amount, var.STASISED[hostmask]) return amount -def allow_deny(cli, nick, chan, rest, mode): - data = rest.split() - msg = None +def decrement_stasis(nick=None): + if nick and nick in var.USERS: + ident = var.USERS[nick]["ident"] + host = var.USERS[nick]["host"] + 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) + 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) + 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) - modes = ("allow", "deny") - assert mode in modes, "mode not in {!r}".format(modes) +def parse_warning_target(target): + if target[0] == "=": + if var.DISABLE_ACCOUNTS: + return (None, None) + tacc = target[1:] + thm = None + elif target in var.USERS: + tacc = var.USERS[target]["account"] + thm = target + "!" + var.USERS[target]["ident"] + "@" + var.USERS[target]["host"] + elif "@" in target: + tacc = None + thm = target + elif not var.DISABLE_ACCOUNTS: + tacc = target + thm = None + else: + return (None, None) + return (tacc, thm) - opts = defaultdict(bool) +def add_warning(target, amount, actor, reason, notes=None, expires=None, need_ack=False, sanctions={}): + tacc, thm = parse_warning_target(target) + if tacc is None and thm is None: + return False - if data and data[0].startswith("-"): - if data[0] == "-cmds": - opts["cmds"] = True - elif data[0] == "-cmd": - if len(data) < 2: - if chan == nick: - pm(cli, nick, messages["no_command_specified"]) + 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"] + + # determine if we need to automatically add any 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: - cli.notice(nick, messages["no_command_specified"]) + 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) + + 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("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 view - views details on warning id + # !warn ack - acknowledges warning id + # Default if only !warn is given is to do !warn list. + pass + +@cmd("fwarn", flag="A", pm=True) +def fwarn(cli, nick, chan, rest): + """Issues a warning to someone or views warnings.""" + # !fwarn list [-all] [nick] [page] + # -all => Shows all warnings, if omitted only shows active (non-expired and non-deleted) ones. + # nick => nick to view warnings for. Can also be a hostmask in nick!user@host form. If nick + # is not online, interpreted as an account name. To specify an account if nick is online, + # use =account. If not specified, shows all warnings on the bot. + # !fwarn view - views details on warning id + # !fwarn del - deletes warning id + # !fwarn add [@] [~expiry] [sanctions] <:reason> [|notes] + # e.g. !fwarn add lykos @1 ~30d deny=goat,stats 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) + nick = None + points = None + need_ack = False + expiry = None + sanctions = {} + reason = None + notes = None + + try: + command = params.pop(0) + except IndexError: + 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", "help"): + reply(cli, nick, chan, messages["fwarn_invalid_subcommand"]) + 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) + nick = params.pop(0) + page = int(params.pop(0)) + if list_all and list_all != "-all": + if nick is not None: + page = int(nick) + nick = list_all + list_all = False + elif show_all == "-all": + list_all = True + except IndexError: + pass + except ValueError: + reply(cli, nick, chan, messages["fwarn_page_invalid"]) + return + if nick is not None: + acc, hm = parse_warning_target(nick) + if acc is None and hm is None: + reply(cli, nick, chan, messages["fwarn_nick_invalid"]) + return + warnings = db.list_warnings(acc, hm, list_all=list_all, skip=(page-1)*10, show=11) + points = db.get_warning_points(acc, hm) + reply(cli, nick, chan, messages["fwarn_list_header"].format(nick, points)) + 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 nick is not None: + parts.append(nick) + parts.append(str(page + 1)) + reply(cli, nick, chan, 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["deleted"]: + start = "\u000314" + end = " [\u00033{0}\u000314]\u0003".format(messages["fwarn_deleted"]) + elif warn["expired"]: + start = "\u000314" + end = " [\u00037{1}\u000314]\u0003".format(messages["fwarn_expired"]) + if not warn["acknowledged"]: + ack = " \u0002!\u0002 " + reply(cli, nick, chan, messages["fwarn_list"].format( + start, ack, warn["id"], warn["issued"], warn["target"], + warn["sender"], warn["reason"], warn["points"], expires, end)) + if i == 0: + reply(cli, nick, chan, messages["fwarn_list_empty"]) + return + + if command != "add": + reply(cli, nick, chan, messages["fwarn_invalid_subcommand"]) + return + + # command == "add" + while params: + p = params.pop(0) + if nick is None: + # figuring out what nick actually is is handled in add_warning + nick = p + 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: + if p[0] == "|": + if p == "|": + notes = "" + else: + notes = p[1:] + continue + reason += " " + p + elif p[0] == ":": + if p == ":": + reason = "" + else: + reason = p[1:] + elif p[0] == "~": + if p == "~": + reply(cli, nick, chan, messages["fwarn_syntax"]) + return + expiry = p[1:] + else: + # sanctions are the only thing left here + sanc = p.split("=", 2) + 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, messages["fwarn_stasis_invalid"]) + return + else: + reply(cli, nick, chan, messages["fwarn_sanction_invalid"]) return - opts["cmd"] = data[1] - data = data[1:] - elif data[0] == "-acc" or data[0] == "-account": - opts["acc"] = True - elif data[0] == "-host": - opts["host"] = True - else: - if chan == nick: - pm(cli, nick, messages["invalid_option"].format(data[0][1:])) - else: - cli.notice(nick, messages["invalid_option"].format(data[0][1:])) + if nick is None or points is None or reason is None: + cli.notice(nick, messages["fwarn_syntax"]) + return + reason = reason.strip() + if notes is not None: + notes = notes.strip() + + # convert expires into a proper datetime + if expires is not None: + suffix = expires[-1] + if suffix == "d": + expires = datetime.now() + timedelta(days=expires[:-1]) + elif suffix == "h": + expires = datetime.now() + timedelta(hours=expires[:-1]) + elif suffix == "m": + expires = datetime.now() + timedelta(minutes=expires[:-1]) + else: + reply(cli, nick, chan, messages["fwarn_expiry_invalid_suffix"]) return - data = data[1:] - - if data and not opts["cmd"]: - lusers = {k.lower(): v for k, v in var.USERS.items()} - user = data[0] - - if opts["acc"] and user != "*": - hostmask = None - acc = user - elif not opts["host"] and user.lower() in lusers: - ident = lusers[user.lower()]["ident"] - host = lusers[user.lower()]["host"] - acc = lusers[user.lower()]["account"] - hostmask = ident + "@" + host - else: - hostmask = user - m = re.match('(?:(?:(.*?)!)?(.*)@)?(.*)', hostmask) - user = m.group(1) or "" - ident = m.group(2) or "" - host = m.group(3) - acc = None - - if user == "*": - opts["host"] = True - - if not var.DISABLE_ACCOUNTS and acc: - if mode == "allow": - variable = var.ALLOW_ACCOUNTS - noaccvar = var.ALLOW - else: - variable = var.DENY_ACCOUNTS - noaccvar = var.DENY - if len(data) == 1: - cmds = set() - if acc in variable: - cmds |= set(variable[acc]) - - if hostmask and not opts["acc"]: - for mask in noaccvar: - if var.match_hostmask(mask, user, ident, host): - cmds |= set(noaccvar[mask]) - - if cmds: - msg = "\u0002{0}\u0002 (Account: {1}) is {2} the following {3}commands: {4}.".format( - data[0], acc, "allowed" if mode == "allow" else "denied", "special " if mode == "allow" else "", ", ".join(cmds)) - else: - msg = "\u0002{0}\u0002 (Account: {1}) is not {2} commands.".format(data[0], acc, "allowed any special" if mode == "allow" else "denied any") - else: - if acc not in variable: - variable[acc] = set() - commands = data[1:] - for command in commands: # Add or remove commands one at a time to a specific account - if "-*" in commands: # Remove all - for cmd in variable[acc]: - if mode == "allow": - var.remove_allow_acc(acc, cmd) - else: - var.remove_deny_acc(acc, cmd) - del variable[acc] - break - if command[0] == "-": # Starting with - (to remove) - rem = True - command = command[1:] - else: - rem = False - if command.startswith(botconfig.CMD_CHAR): # ignore command prefix - command = command[len(botconfig.CMD_CHAR):] - - if not rem: - if command in COMMANDS and command not in ("fdeny", "fallow", "fsend", "exec", "eval") and command not in variable[acc]: - variable[acc].add(command) - if mode == "allow": - var.add_allow_acc(acc, command) - else: - var.add_deny_acc(acc, command) - elif command in variable[acc]: - variable[acc].remove(command) - if mode == "allow": - var.remove_allow_acc(acc, command) - else: - var.remove_deny_acc(acc, command) - if acc in variable and variable[acc]: - msg = "\u0002{0}\u0002 (Account: {1}) is now {2} the following {3}commands: {4}{5}.".format( - data[0], acc, "allowed" if mode == "allow" else "denied", "special " if mode == "allow" else "", botconfig.CMD_CHAR, ", {0}".format(botconfig.CMD_CHAR).join(variable[acc])) - else: - if acc in variable: - del variable[acc] - msg = "\u0002{0}\u0002 (Account: {1}) is no longer {2} commands.".format(data[0], acc, "allowed any special" if mode == 'allow' else "denied any") - elif var.ACCOUNTS_ONLY and not opts["host"]: - msg = "Error: \u0002{0}\u0002 is not logged in to NickServ.".format(data[0]) - else: - if mode == "allow": - variable = var.ALLOW - else: - variable = var.DENY - if len(data) == 1: # List commands for a specific hostmask - cmds = [] - for mask in variable: - if var.match_hostmask(mask, user, ident, host): - cmds.extend(variable[mask]) - - if cmds: - msg = "\u0002{0}\u0002 (Host: {1}) is {2} the following {3}commands: {4}.".format( - data[0], hostmask, "allowed" if mode == "allow" else "denied", "special " if mode == "allow" else "", ", ".join(cmds)) - else: - msg = "\u0002{0}\u0002 (Host: {1}) is not {2} commands.".format(data[0], hostmask, "allowed any special" if mode == "allow" else "denied any") - else: - if hostmask not in variable: - variable[hostmask] = set() - commands = data[1:] - for command in commands: #add or remove commands one at a time to a specific hostmask - if "-*" in commands: # Remove all - for cmd in variable[hostmask]: - if mode == "allow": - var.remove_allow(hostmask, cmd) - else: - var.remove_deny(hostmask, cmd) - del variable[hostmask] - break - if command[0] == '-': #starting with - removes - rem = True - command = command[1:] - else: - rem = False - if command.startswith(botconfig.CMD_CHAR): #ignore command prefix - command = command[len(botconfig.CMD_CHAR):] - - if not rem: - if command in COMMANDS and command not in ("fdeny", "fallow", "fsend", "exec", "eval") and command not in variable[hostmask]: - variable[hostmask].add(command) - if mode == "allow": - var.add_allow(hostmask, command) - else: - var.add_deny(hostmask, command) - elif command in variable[hostmask]: - variable[hostmask].remove(command) - if mode == "allow": - var.remove_allow(hostmask, command) - else: - var.remove_deny(hostmask, command) - - if hostmask in variable and variable[hostmask]: - msg = "\u0002{0}\u0002 (Host: {1}) is now {2} the following {3}commands: {4}{5}.".format( - data[0], hostmask, "allowed" if mode == "allow" else "denied", "special " if mode == "allow" else "", botconfig.CMD_CHAR, ", {0}".format(botconfig.CMD_CHAR).join(variable[hostmask])) - else: - if hostmask in variable: - del variable[hostmask] - msg = "\u0002{0}\u0002 (Host: {1}) is no longer {2} commands.".format(data[0], hostmask, "allowed any special" if mode == "allow" else "denied any") - - else: - users_to_cmds = {} - if not var.DISABLE_ACCOUNTS and not opts["host"]: - if mode == "allow": - variable = var.ALLOW_ACCOUNTS - noaccvar = var.ALLOW - else: - variable = var.DENY_ACCOUNTS - noaccvar = var.DENY - - if variable: - for acc, varied in variable.items(): - if opts["acc"] or (var.ACCOUNTS_ONLY and not noaccvar): - users_to_cmds[acc] = sorted(varied, key=str.lower) - else: - users_to_cmds[acc+" (Account)"] = sorted(varied, key=str.lower) - if not opts["acc"]: - if mode == "allow": - variable = var.ALLOW - else: - variable = var.DENY - if variable: - for hostmask, varied in variable.items(): - if var.DISABLE_ACCOUNTS or opts["host"]: - users_to_cmds[hostmask] = sorted(varied, key=str.lower) - else: - users_to_cmds[hostmask+" (Host)"] = sorted(varied, key=str.lower) - - - if not users_to_cmds: # Deny or Allow list is empty - msg = "Nobody is {0} commands.".format("allowed any special" if mode == "allow" else "denied any") - else: - if opts["cmds"] or opts["cmd"]: - cmds_to_users = defaultdict(list) - - for user in sorted(users_to_cmds, key=str.lower): - for cmd in users_to_cmds[user]: - cmds_to_users[cmd].append(user) - - if opts["cmd"]: - cmd = opts["cmd"] - users = cmds_to_users[cmd] - - if cmd not in COMMANDS: - if chan == nick: - pm(cli, nick, messages["command_does_not_exist"]) - else: - cli.notice(nick, messages["command_does_not_exist"]) - - return - - if users: - msg = "\u0002{0}{1}\u0002 is {2} to the following people: {3}".format( - botconfig.CMD_CHAR, opts["cmd"], "allowed" if mode == "allow" else "denied", ", ".join(users)) - else: - msg = "\u0002{0}{1}\u0002 is not {2} to any special people.".format( - botconfig.CMD_CHAR, opts["cmd"], "allowed" if mode == "allow" else "denied") - else: - msg = "{0}: {1}".format("Allowed" if mode == "allow" else "Denied", "; ".join("\u0002{0}\u0002 ({1})".format( - cmd, ", ".join(users)) for cmd, users in sorted(cmds_to_users.items(), key=lambda t: t[0].lower()))) - else: - msg = "{0}: {1}".format("Allowed" if mode == "allow" else "Denied", "; ".join("\u0002{0}\u0002 ({1})".format( - user, ", ".join(cmds)) for user, cmds in sorted(users_to_cmds.items(), key=lambda t: t[0].lower()))) - - if msg: - msg = var.break_long_message(msg.split("; "), "; ") - - if chan == nick: - pm(cli, nick, msg) - else: - cli.msg(chan, msg) - -@cmd("fallow", admin_only=True, pm=True) -def fallow(cli, nick, chan, rest): - """Allow someone to use an admin command.""" - allow_deny(cli, nick, chan, rest, "allow") - -@cmd("fdeny", admin_only=True, pm=True) -def fdeny(cli, nick, chan, rest): - """Deny someone from using a command.""" - allow_deny(cli, nick, chan, rest, "deny") - @cmd("wait", "w", playing=True, phases=("join",)) def wait(cli, nick, chan, rest): """Increases the wait time until !start can be used.""" @@ -8347,7 +8252,7 @@ def wait(cli, nick, chan, rest): cli.msg(chan, messages["wait_time_increase"].format(nick, var.EXTRA_WAIT)) -@cmd("fwait", admin_only=True, phases=("join",)) +@cmd("fwait", flag="A", phases=("join",)) def fwait(cli, nick, chan, rest): """Forces an increase (or decrease) in wait time. Can be used with a number of seconds to wait.""" @@ -8374,7 +8279,7 @@ def fwait(cli, nick, chan, rest): cli.msg(chan, messages["forced_wait_time_decrease"].format(nick, abs(extra), "s" if extra != -1 else "")) -@cmd("fstop", admin_only=True, phases=("join", "day", "night")) +@cmd("fstop", flag="A", phases=("join", "day", "night")) def reset_game(cli, nick, chan, rest): """Forces the game to stop.""" if nick == "": @@ -8442,13 +8347,13 @@ def get_help(cli, rnick, chan, rest): # if command was not found, or if no command was given: for name, fn in COMMANDS.items(): - if (name and not fn[0].admin_only and not fn[0].owner_only and + if (name and not fn[0].flag and not fn[0].owner_only and name not in fn[0].aliases and fn[0].chan): fns.append("{0}{1}{0}".format("\u0002", name)) afns = [] if is_admin(nick, ident, host): for name, fn in COMMANDS.items(): - if fn[0].admin_only and name not in fn[0].aliases: + if fn[0].flag and name not in fn[0].aliases: afns.append("{0}{1}{0}".format("\u0002", name)) fns.sort() # Output commands in alphabetical order if chan == nick: @@ -8511,7 +8416,7 @@ def on_invite(cli, raw_nick, something, chan): else: pm(cli, parse_nick(nick)[0], messages["not_an_admin"]) -@cmd("fpart", raw_nick=True, admin_only=True, pm=True) +@cmd("fpart", raw_nick=True, flag="A", pm=True) def fpart(cli, rnick, chan, rest): """Makes the bot forcibly leave a channel.""" nick = parse_nick(rnick)[0] @@ -8831,7 +8736,7 @@ def myrole(cli, nick, chan, rest): message += "." pm(cli, nick, message) -@cmd("faftergame", admin_only=True, raw_nick=True, pm=True) +@cmd("faftergame", flag="D", raw_nick=True, pm=True) def aftergame(cli, rawnick, chan, rest): """Schedule a command to be run after the current game.""" nick = parse_nick(rawnick)[0] @@ -8865,7 +8770,7 @@ def aftergame(cli, rawnick, chan, rest): var.AFTER_FLASTGAME = do_action -@cmd("flastgame", admin_only=True, raw_nick=True, pm=True) +@cmd("flastgame", flag="D", raw_nick=True, pm=True) def flastgame(cli, rawnick, chan, rest): """Disables starting or joining a game, and optionally schedules a command to run after the current game ends.""" nick, _, ident, host = parse_nick(rawnick) @@ -8917,10 +8822,10 @@ def game_stats(cli, nick, chan, rest): # List all games sizes and totals if no size is given if not gamesize: - reply(cli, nick, chan, var.get_game_totals(gamemode)) + reply(cli, nick, chan, db.get_game_totals(gamemode)) else: # Attempt to find game stats for the given game size - reply(cli, nick, chan, var.get_game_stats(gamemode, gamesize)) + reply(cli, nick, chan, db.get_game_stats(gamemode, gamesize)) @cmd("playerstats", "pstats", "player", "p", pm=True) def player_stats(cli, nick, chan, rest): @@ -8949,21 +8854,25 @@ def player_stats(cli, nick, chan, rest): # Find the player's account if possible luser = user.lower() lusers = {k.lower(): v for k, v in var.USERS.items()} - if luser in lusers and not var.DISABLE_ACCOUNTS: + if luser in lusers: acc = lusers[luser]["account"] - if acc == "*": + hostmask = luser + "!" + lusers[luser]["ident"] + "@" + lusers[luser]["host"] + if acc == "*" and var.ACCOUNTS_ONLY: if luser == nick.lower(): cli.notice(nick, messages["not_logged_in"]) else: cli.notice(nick, messages["account_not_logged_in"].format(user)) - return + elif "@" in user: + acc = None + hostmask = user else: acc = user + hostmask = None # List the player's total games for all roles if no role is given if len(params) < 2: - reply(cli, nick, chan, var.get_player_totals(acc), private=True) + reply(cli, nick, chan, db.get_player_totals(acc, hostmask), private=True) else: role = " ".join(params[1:]) if role not in var.ROLE_GUIDE.keys(): @@ -8973,7 +8882,7 @@ def player_stats(cli, nick, chan, rest): return role = match # Attempt to find the player's stats - reply(cli, nick, chan, var.get_player_stats(acc, role)) + reply(cli, nick, chan, db.get_player_stats(acc, hostmask, role)) @cmd("mystats", "m", pm=True) def my_stats(cli, nick, chan, rest): @@ -9041,7 +8950,7 @@ def vote(cli, nick, chan, rest): else: return show_votes.caller(cli, nick, chan, rest) -@cmd("fpull", admin_only=True, pm=True) +@cmd("fpull", flag="D", pm=True) def fpull(cli, nick, chan, rest): """Pulls from the repository to update the bot.""" @@ -9073,7 +8982,7 @@ def fpull(cli, nick, chan, rest): else: pm(cli, nick, messages["process_exited"] % (command, cause, ret)) -@cmd("fsend", admin_only=True, pm=True) +@cmd("fsend", flag="F", pm=True) def fsend(cli, nick, chan, rest): """Forcibly send raw IRC commands to the server.""" cli.send(rest) @@ -9108,12 +9017,12 @@ def _say(cli, raw_nick, rest, command, action=False): cli.send("PRIVMSG {0} :{1}".format(target, message)) -@cmd("fsay", admin_only=True, raw_nick=True, pm=True) +@cmd("fsay", flag="s", raw_nick=True, pm=True) def fsay(cli, raw_nick, chan, rest): """Talk through the bot as a normal message.""" _say(cli, raw_nick, rest, "fsay") -@cmd("fact", "fdo", "fme", admin_only=True, raw_nick=True, pm=True) +@cmd("fact", "fdo", "fme", flag="s", raw_nick=True, pm=True) def fact(cli, raw_nick, chan, rest): """Act through the bot as an action.""" _say(cli, raw_nick, rest, "fact", action=True) @@ -9140,7 +9049,7 @@ def can_run_restricted_cmd(nick): return True -@cmd("fspectate", admin_only=True, pm=True, phases=("day", "night")) +@cmd("fspectate", flag="A", pm=True, phases=("day", "night")) def fspectate(cli, nick, chan, rest): """Spectate wolfchat or deadchat.""" if not can_run_restricted_cmd(nick): @@ -9206,7 +9115,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: except Exception as e: cli.msg(chan, str(type(e))+":"+str(e)) - @cmd("revealroles", admin_only=True, pm=True, phases=("day", "night")) + @cmd("revealroles", flag="a", pm=True, phases=("day", "night")) def revealroles(cli, nick, chan, rest): """Reveal role information.""" @@ -9317,7 +9226,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: cli.notice(nick, var.break_long_message(output, " | ")) - @cmd("fgame", admin_only=True, raw_nick=True, phases=("join",)) + @cmd("fgame", flag="d", raw_nick=True, phases=("join",)) def fgame(cli, nick, chan, rest): """Force a certain game mode to be picked. Disable voting for game modes upon use.""" nick = parse_nick(nick)[0] @@ -9366,7 +9275,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: # DO NOT MAKE THIS A PMCOMMAND ALSO - @cmd("force", admin_only=True) + @cmd("force", flag="d") def force(cli, nick, chan, rest): """Force a certain player to use a specific command.""" rst = re.split(" +",rest) @@ -9395,7 +9304,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: for fn in COMMANDS[comm]: if fn.owner_only: continue - if fn.admin_only and nick in var.USERS and not is_admin(nick): + if fn.flag and nick in var.USERS and not is_admin(nick): # Not a full admin cli.notice(nick, messages["admin_only_force"]) continue @@ -9409,7 +9318,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: cli.msg(chan, messages["command_not_found"]) - @cmd("rforce", admin_only=True) + @cmd("rforce", flag="d") def rforce(cli, nick, chan, rest): """Force all players of a given role to perform a certain action.""" rst = re.split(" +",rest) @@ -9435,7 +9344,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: for fn in COMMANDS[comm]: if fn.owner_only: continue - if fn.admin_only and nick in var.USERS and not is_admin(nick): + if fn.flag and nick in var.USERS and not is_admin(nick): # Not a full admin cli.notice(nick, messages["admin_only_force"]) continue @@ -9450,7 +9359,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: - @cmd("frole", admin_only=True, phases=("day", "night")) + @cmd("frole", flag="d", phases=("day", "night")) def frole(cli, nick, chan, rest): """Change the role or template of a player.""" rst = re.split(" +",rest) From bba5ab745eb268fa3361a7f6b7ec89f7b0ba1929 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Mon, 6 Jun 2016 18:44:45 -0500 Subject: [PATCH 02/10] Make bot run and fwarn partially work --- messages/en.json | 22 ++++-- src/db.py | 158 +++++++++++++++++++++++++++++++++++-------- src/decorators.py | 17 +++-- src/wolfgame.py | 168 ++++++++++++++++++++++++++++++++++++---------- 4 files changed, 293 insertions(+), 72 deletions(-) diff --git a/messages/en.json b/messages/en.json index b5aeb60..dec35d5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -796,27 +796,41 @@ "villagergame_win": "Game over! The villagers come to their senses and realize there are actually no wolves, and live in harmony forevermore. Everybody wins.", "villagergame_nope": "Game over! The villagers decided incorrectly that there are actually no wolves, allowing the wolves to slaughter the remainder of them in their sleep with impunity.", "stop_bot_ingame_safeguard": "Warning: A game is currently running. If you want to {what} the bot anyway, use \"{prefix}{cmd} -force\".", - "fwarn_usage": "Usage: fwarn list|view|add|del|help. See fwarn help for more details.", + "fwarn_usage": "Usage: fwarn list|view|add|del|set|help. See fwarn help for more details.", "fwarn_list_syntax": "Usage: fwarn list [-all] [nick[!user@host]|=account] [page]", "fwarn_view_syntax": "Usage: fwarn view ", - "fwarn_del_syntax": "Usage: fwarn del ", + "fwarn_del_syntax": "Usage: fwarn del ", + "fwarn_set_syntax": "Usage: fwarn set [| notes]", "fwarn_help_syntax": "Usage: fwarn help ", - "fwarn_add_syntax": "Usage: fwarn add [@] [~expiry] [sanctions] <:reason> [|notes]", + "fwarn_add_syntax": "Usage: fwarn add [@] [~expiry] [sanctions] <:reason> [| notes]", "fwarn_page_invalid": "Invalid page, must be a number 1 or greater.", "fwarn_points_invalid": "Invalid points, must be a number above 0.", "fwarn_expiry_invalid_suffix": "Invalid expiration suffix, must use either d, h, or m.", "fwarn_cannot_add": "Cannot add warning, double-check your parameters (the nick might be wrong or you are not joined to the channel).", + "fwarn_added": "Added warning {0}.", + "fwarn_done": "Done.", "fwarn_sanction_invalid": "Invalid sanction, can be either deny or stasis.", "fwarn_stasis_invalid": "Invalid stasis amount, specify sanction as stasis=number.", "fwarn_deny_invalid": "Invalid denied commands, specify sanction as deny=command,command,command without spaces.", "fwarn_deny_invalid_command": "Invalid command \"{0}\", specify sanction as deny=command,command,command without spaces.", "fwarn_list_header": "{0} has {1} active warning points.", - "fwarn_list": "{0}{1}[#{2} {3}] {4} by {5} - {6} ({7}, {8}){9}", + "fwarn_list": "{0}{1}[#{2} {3}] to {4} by {5} - {6} ({7} points, {8}){9}", "fwarn_deleted": "deleted", "fwarn_expired": "expired", "fwarn_never_expires": "never expires", "fwarn_list_footer": "More results are available, use fwarn list {0} to view them.", "fwarn_list_empty": "No results.", + "fwarn_invalid_warning": "The specified warning id does not exist or you do not have permission to view it.", + "fwarn_view_header": "Warning #{0}, given to {1} on {2} by {3}. {4} points. {5}.", + "fwarn_view_active": "Currently active, {0}", + "fwarn_view_expires": "expires on {0}", + "fwarn_view_expired": "Expired on {0}", + "fwarn_view_deleted": "Deleted on {0} by {1}", + "fwarn_view_ack": "Warning has not yet been acknowledged.", + "fwarn_view_sanctions": "Sanctions:", + "fwarn_view_stasis": "{0} games of stasis.", + "fwarn_view_deny": "denied {0}.", + "fwarn_reason_required": "A public warning reason is required.", "_": " vim: set sw=4 expandtab:" } diff --git a/src/db.py b/src/db.py index 29a052c..38e4fc1 100644 --- a/src/db.py +++ b/src/db.py @@ -1,3 +1,4 @@ +import botconfig import src.settings as var import sqlite3 import os @@ -265,6 +266,8 @@ def get_flags(acc, hostmask): ON at.id = a.template WHERE a.person = ?""", (peid,)) row = c.fetchone() + if row is None: + return "" return row[0] def get_denied_commands(acc, hostmask): @@ -312,17 +315,18 @@ def list_all_warnings(list_all=False, skip=0, show=0): warning.issued, warning.expires, CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') - THEN 1 ELSE 0 END AS expired, + THEN 0 ELSE 1 END AS expired, warning.acknowledged, warning.deleted, warning.reason + FROM warning JOIN person pet ON pet.id = warning.target JOIN player plt ON plt.id = pet.primary_player - LEFT JOIN pes + LEFT JOIN person pes ON pes.id = warning.sender - LEFT JOIN pls + LEFT JOIN player pls ON pls.id = pes.primary_player """ if not list_all: @@ -348,8 +352,8 @@ def list_all_warnings(list_all=False, skip=0, show=0): "expires": row[5], "expired": row[6], "ack": row[7], - "deleted": row[8]}, - "reason": row[9]) + "deleted": row[8], + "reason": row[9]}) return warnings def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): @@ -363,17 +367,18 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): warning.issued, warning.expires, CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') - THEN 1 ELSE 0 END AS expired, + THEN 0 ELSE 1 END AS expired, warning.acknowledged, warning.deleted, warning.reason + FROM warning JOIN person pet ON pet.id = warning.target JOIN player plt ON plt.id = pet.primary_player - LEFT JOIN pes + LEFT JOIN person pes ON pes.id = warning.sender - LEFT JOIN pls + LEFT JOIN player pls ON pls.id = pes.primary_player WHERE warning.target = ? @@ -404,32 +409,129 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): "reason": row[9]}) return warnings +def get_warning(warn_id, acc=None, hm=None): + pe, pl = _get_ids(acc, hm) + c = conn.cursor() + sql = """SELECT + warning.id, + COALESCE(plt.account, plt.hostmask) AS target, + COALESCE(pls.account, pls.hostmask, ?) AS sender, + warning.amount, + warning.issued, + warning.expires, + CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') + THEN 0 ELSE 1 END AS expired, + warning.acknowledged, + warning.deleted, + warning.reason, + warning.notes, + COALESCE(pld.account, pld.hostmask) AS deleted_by, + warning.deleted_on + FROM warning + JOIN person pet + ON pet.id = warning.target + JOIN player plt + ON plt.id = pet.primary_player + LEFT JOIN person pes + ON pes.id = warning.sender + LEFT JOIN player pls + ON pls.id = pes.primary_player + LEFT JOIN person ped + ON ped.id = warning.deleted_by + LEFT JOIN player pld + ON pld.id = ped.primary_player + WHERE + warning.id = ? + """ + params = (botconfig.NICK, warn_id) + if acc is not None and hm is not None: + sql += """ AND warning.target = ? + AND warning.deleted = 0""" + params = (botconfig.NICK, warn_id, peid) + + c.execute(sql, params) + row = c.fetchone() + if not row: + return None + + return {"id": row[0], + "target": row[1], + "sender": row[2], + "amount": row[3], + "issued": row[4], + "expires": row[5], + "expired": row[6], + "ack": row[7], + "deleted": row[8], + "reason": row[9], + "notes": row[10], + "deleted_by": row[11], + "deleted_on": row[12], + "sanctions": get_warning_sanctions(warn_id)} + +def get_warning_sanctions(warn_id): + c = conn.cursor() + c.execute("SELECT sanction, data FROM warning_sanction WHERE warning=?", (warn_id,)) + sanctions = {} + for sanc, data in c: + if sanc == "stasis": + sanctions["stasis"] = int(data) + elif sanc == "deny command": + if "deny" not in sanctions: + sanctions["deny"] = set() + sanctions["deny"].add(data) + + return sanctions + def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack): teid, tlid = _get_ids(tacc, thm) seid, slid = _get_ids(sacc, shm) - c = conn.cursor() - c.execute("""INSERT INTO warning - ( - target, sender, amount, - issued, expires, - reasons, notes, - acknowledged - ) - VALUES - ( - ?, ?, ?, - datetime('now'), ?, - ?, ?, - ? - )""", (teid, seid, amount, expires, reasons, notes, not need_ack)) + with conn: + c = conn.cursor() + c.execute("""INSERT INTO warning + ( + target, sender, amount, + issued, expires, + reasons, notes, + acknowledged + ) + VALUES + ( + ?, ?, ?, + datetime('now'), ?, + ?, ?, + ? + )""", (teid, seid, amount, expires, reasons, notes, not need_ack)) return c.lastrowid def add_warning_sanction(warning, sanction, data): - c = conn.cursor() - c.execute("""INSERT INTO warning_sanction - (warning, sanction, data) - VALUES - (?, ?, ?)""", (warning, sanction, data)) + with conn: + c = conn.cursor() + c.execute("""INSERT INTO warning_sanction + (warning, sanction, data) + VALUES + (?, ?, ?)""", (warning, sanction, data)) + +def del_warning(warning, acc, hm): + peid, plid = _get_ids(acc, hm) + with conn: + c = conn.cursor() + c.execute("""UPDATE warning + SET + acknowledged = 1, + deleted = 1, + deleted_on = datetime('now'), + deleted_by = ? + WHERE + id = ? + AND deleted = 0""", (peid, warning)) + +def set_warning(warning, reason, notes): + with conn: + c = conn.cursor() + c.execute("""UPDATE warning + SET reason = ?, notes = ? + WHERE id = ?""", (reason, notes, warning)) def _upgrade(): # no upgrades yet, once there are some, add methods like _add_table(), _add_column(), etc. diff --git a/src/decorators.py b/src/decorators.py index 54341d6..853b286 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -10,7 +10,7 @@ from oyoyo.parse import parse_nick import botconfig import src.settings as var from src.utilities import * -from src import logger +from src import logger, db from src.messages import messages adminlog = logger("audit.log") @@ -176,8 +176,9 @@ class cmd: forced_owner_only = True break + is_owner = var.is_owner(nick, ident, host) if self.owner_only or forced_owner_only: - if var.is_owner(nick, ident, host): + if is_owner: adminlog(chan, rawnick, self.name, rest) return self.func(*largs) @@ -190,13 +191,14 @@ class cmd: # TODO: cache flags and cmds (below) on init, possibly store in var.USERS # that would greatly reduce our db calls flags = db.get_flags(acc, hostmask) - if self.flag and self.flag in flags: + is_full_admin = "F" in flags + if self.flag and (is_full_admin or is_owner): adminlog(chan, rawnick, self.name, rest) return self.func(*largs) denied_cmds = db.get_denied_commands(acc, hostmask) for command in self.cmds: - if command in denied_commands: + if command in denied_cmds: if chan == nick: pm(cli, nick, messages["invalid_permissions"]) else: @@ -204,7 +206,10 @@ class cmd: return if self.flag: - if chan == nick: + if self.flag in flags: + adminlog(chan, rawnick, self.name, rest) + return self.func(*largs) + elif chan == nick: pm(cli, nick, messages["not_an_admin"]) else: cli.notice(nick, messages["not_an_admin"]) @@ -240,3 +245,5 @@ class hook: HOOKS[each].remove(inner) if not HOOKS[each]: del HOOKS[each] + +# vim: set sw=4 expandtab: diff --git a/src/wolfgame.py b/src/wolfgame.py index 5bbf539..ccc39fc 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -8018,9 +8018,10 @@ def fwarn(cli, nick, chan, rest): # is not online, interpreted as an account name. To specify an account if nick is online, # use =account. If not specified, shows all warnings on the bot. # !fwarn view - views details on warning id - # !fwarn del - deletes warning id - # !fwarn add [@] [~expiry] [sanctions] <:reason> [|notes] - # e.g. !fwarn add lykos @1 ~30d deny=goat,stats stasis=5 :Spamming|I secretly just hate him + # !fwarn del - deletes warning id + # !fwarn set [| notes] + # !fwarn add [@] [~expiry] [sanctions] <:reason> [| notes] + # e.g. !fwarn add lykos @1 ~30d deny=goat,gstats stasis=5 :Spamming | I secretly just hate him # nick => nick to warn. Can also be a hostmask in nick!user@host form. If nick is not online, # interpreted as an account name. To specify an account if nick is online, use =account. # @ => warning requires acknowledgement before user can !join again @@ -8035,7 +8036,7 @@ def fwarn(cli, nick, chan, rest): # in reasons (no escaping is performed). params = re.split(" +", rest) - nick = None + target = None points = None need_ack = False expiry = None @@ -8055,8 +8056,8 @@ def fwarn(cli, nick, chan, rest): except IndexError: reply(cli, nick, chan, messages["fwarn_help_syntax"]) return - if subcommand not in ("list", "view", "add", "del", "help"): - reply(cli, nick, chan, messages["fwarn_invalid_subcommand"]) + if subcommand not in ("list", "view", "add", "del", "set", "help"): + reply(cli, nick, chan, messages["fwarn_usage"]) return reply(cli, nick, chan, messages["fwarn_{0}_syntax".format(subcommand)]) return @@ -8066,12 +8067,12 @@ def fwarn(cli, nick, chan, rest): page = 1 try: list_all = params.pop(0) - nick = params.pop(0) + target = params.pop(0) page = int(params.pop(0)) if list_all and list_all != "-all": - if nick is not None: - page = int(nick) - nick = list_all + if target is not None: + page = int(target) + target = list_all list_all = False elif show_all == "-all": list_all = True @@ -8080,14 +8081,14 @@ def fwarn(cli, nick, chan, rest): except ValueError: reply(cli, nick, chan, messages["fwarn_page_invalid"]) return - if nick is not None: - acc, hm = parse_warning_target(nick) + if target is not None: + acc, hm = parse_warning_target(target) if acc is None and hm is None: reply(cli, nick, chan, messages["fwarn_nick_invalid"]) return warnings = db.list_warnings(acc, hm, list_all=list_all, skip=(page-1)*10, show=11) points = db.get_warning_points(acc, hm) - reply(cli, nick, chan, messages["fwarn_list_header"].format(nick, points)) + reply(cli, nick, chan, messages["fwarn_list_header"].format(target, points)) else: warnings = db.list_all_warnings(list_all=list_all, skip=(page-1)*10, show=11) @@ -8098,8 +8099,8 @@ def fwarn(cli, nick, chan, rest): parts = [] if list_all: parts.append("-all") - if nick is not None: - parts.append(nick) + if target is not None: + parts.append(target) parts.append(str(page + 1)) reply(cli, nick, chan, messages["fwarn_list_footer"].format(" ".join(parts))) break @@ -8109,29 +8110,123 @@ def fwarn(cli, nick, chan, rest): expires = warn["expires"] if warn["expires"] is not None else messages["fwarn_never_expires"] if warn["deleted"]: start = "\u000314" - end = " [\u00033{0}\u000314]\u0003".format(messages["fwarn_deleted"]) + end = " [\u00034{0}\u000314]\u0003".format(messages["fwarn_deleted"]) elif warn["expired"]: start = "\u000314" - end = " [\u00037{1}\u000314]\u0003".format(messages["fwarn_expired"]) - if not warn["acknowledged"]: - ack = " \u0002!\u0002 " + end = " [\u00037{0}\u000314]\u0003".format(messages["fwarn_expired"]) + if not warn["ack"]: + ack = "\u0002!\u0002 " reply(cli, nick, chan, messages["fwarn_list"].format( start, ack, warn["id"], warn["issued"], warn["target"], - warn["sender"], warn["reason"], warn["points"], expires, end)) + warn["sender"], warn["reason"], warn["amount"], expires, end)) if i == 0: reply(cli, nick, chan, messages["fwarn_list_empty"]) return + if command == "view": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["fwarn_view_syntax"]) + return + + warning = db.get_warning(warn_id) + if warning is None: + reply(cli, nick, chan, messages["fwarn_invalid_warning"]) + return + + if warning["deleted"]: + expires = messages["fwarn_view_deleted"].format(warning["deleted_on"], warning["deleted_by"]) + elif warning["expired"]: + expires = messages["fwarn_view_expired"].format(warning["expires"]) + elif warning["expires"] is None: + expires = messages["fwarn_view_active"].format(messages["fwarn_never_expires"]) + else: + expires = messages["fwarn_view_active"].format(messages["fwarn_view_expires"].format(warning["expires"])) + + reply(cli, nick, chan, messages["fwarn_view_header"].format( + warning["id"], warning["target"], warning["issued"], warning["sender"], + warning["amount"], expires)) + + reason = [warning["reason"]] + if warning["notes"] is not None: + reason.append(warning["notes"]) + reply(cli, nick, chan, " | ".join(reason)) + + sanctions = [] + if not warning["ack"]: + sanctions.append(messages["fwarn_view_ack"]) + if warning["sanctions"]: + sanctions.append(messages["fwarn_view_sanctions"]) + if "stasis" in warning["sanctions"]: + sanctions.append(messages["fwarn_view_stasis"].format(warning["sanctions"]["stasis"])) + if "deny" in warning["sanctions"]: + sanctions.append(messages["fwarn_view_deny"].format(", ".join(warning["sanctions"]["deny"]))) + if sanctions: + reply(cli, nick, chan, " ".join(sanctions)) + return + + if command == "del": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["fwarn_del_syntax"]) + return + + warning = db.get_warning(warn_id) + if warning is None: + reply(cli, nick, chan, messages["fwarn_invalid_warning"]) + return + + acc, hm = parse_warning_target(nick) + db.del_warning(warn_id, acc, hm) + reply(cli, nick, chan, messages["fwarn_done"]) + return + + if command == "set": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["fwarn_del_syntax"]) + return + + warning = db.get_warning(warn_id) + if warning is None: + reply(cli, nick, chan, messages["fwarn_invalid_warning"]) + return + + rsp = " ".join(params).split("|", 1) + if len(rsp) == 1: + rsp.append(None) + reason, notes = rsp + + reason = reason.strip() + if not reason: + reply(cli, nick, chan, messages["fwarn_reason_required"]) + return + + # maintain existing notes if none were specified + if notes is not None: + notes = notes.strip() + if not notes: + notes = None + else: + notes = warning["notes"] + + db.set_warning(warn_id, reason, notes) + reply(cli, nick, chan, messages["fwarn_done"]) + return + if command != "add": - reply(cli, nick, chan, messages["fwarn_invalid_subcommand"]) + reply(cli, nick, chan, messages["fwarn_usage"]) return # command == "add" while params: p = params.pop(0) - if nick is None: - # figuring out what nick actually is is handled in add_warning - nick = p + if target is None: + # figuring out what target actually is is handled in add_warning + target = p elif points is None: points = p if points[0] == "@": @@ -8148,13 +8243,10 @@ def fwarn(cli, nick, chan, rest): elif notes is not None: notes += " " + p elif reason is not None: - if p[0] == "|": - if p == "|": - notes = "" - else: - notes = p[1:] - continue - reason += " " + p + rsp = p.split("|", 1) + if len(rsp) > 1: + notes = rsp[1] + reason += " " + rsp[0] elif p[0] == ":": if p == ":": reason = "" @@ -8167,7 +8259,7 @@ def fwarn(cli, nick, chan, rest): expiry = p[1:] else: # sanctions are the only thing left here - sanc = p.split("=", 2) + sanc = p.split("=", 1) if sanc[0] == "deny": try: cmds = sanc[1].split(",") @@ -8193,15 +8285,15 @@ def fwarn(cli, nick, chan, rest): elif sanc[0] == "stasis": try: sanctions["stasis"] = int(sanc[1]) - except IndexError, ValueError: + except (IndexError, ValueError): reply(cli, nick, messages["fwarn_stasis_invalid"]) return else: reply(cli, nick, chan, messages["fwarn_sanction_invalid"]) return - if nick is None or points is None or reason is None: - cli.notice(nick, messages["fwarn_syntax"]) + if target is None or points is None or reason is None: + reply(cli, nick, chan, messages["fwarn_add_syntax"]) return reason = reason.strip() @@ -8221,6 +8313,12 @@ def fwarn(cli, nick, chan, rest): reply(cli, nick, chan, messages["fwarn_expiry_invalid_suffix"]) return + warn_id = add_warning(target, amount, nick, reason, notes, expires, need_ack, sanctions) + if warn_id is False: + reply(cli, nick, chan, messages["fwarn_cannot_add"]) + else: + reply(cli, nick, chan, messages["fwarn_added"].format(warn_id)) + @cmd("wait", "w", playing=True, phases=("join",)) def wait(cli, nick, chan, rest): """Increases the wait time until !start can be used.""" From a6ea55a8fee88d3104cab672d76b08dc6442487e Mon Sep 17 00:00:00 2001 From: skizzerz Date: Tue, 7 Jun 2016 14:17:21 -0500 Subject: [PATCH 03/10] 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 --- messages/en.json | 19 ++- src/__init__.py | 4 - src/db.py | 236 ++++++++++++++++++++++---------- src/decorators.py | 6 +- src/settings.py | 23 +--- src/wolfgame.py | 342 ++++++++++++++++++++++++++++++++++++++++------ 6 files changed, 492 insertions(+), 138 deletions(-) diff --git a/messages/en.json b/messages/en.json index dec35d5..1f0060e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -574,6 +574,7 @@ "account_not_in_stasis": "\u0002{0}\u0002 (Account: {1}) is not in stasis.", "currently_stasised": "Currently stasised: {0}", "noone_stasised": "Nobody is currently stasised.", + "stasis_cannot_increase": "Cannot increase stasis using fstasis; use fwarn instead.", "no_command_specified": "Error: No command specified. Did you mean \u0002-cmds\u0002?", "invalid_option": "Invalid option: {0}", "command_does_not_exist": "That command does not exist.", @@ -797,14 +798,20 @@ "villagergame_nope": "Game over! The villagers decided incorrectly that there are actually no wolves, allowing the wolves to slaughter the remainder of them in their sleep with impunity.", "stop_bot_ingame_safeguard": "Warning: A game is currently running. If you want to {what} the bot anyway, use \"{prefix}{cmd} -force\".", "fwarn_usage": "Usage: fwarn list|view|add|del|set|help. See fwarn help for more details.", + "warn_usage": "Usage: warn list|view|ack|help. See warn help for more details.", "fwarn_list_syntax": "Usage: fwarn list [-all] [nick[!user@host]|=account] [page]", "fwarn_view_syntax": "Usage: fwarn view ", "fwarn_del_syntax": "Usage: fwarn del ", "fwarn_set_syntax": "Usage: fwarn set [| notes]", "fwarn_help_syntax": "Usage: fwarn help ", + "warn_list_syntax": "Usage: warn list [-all] [page]", + "warn_view_syntax": "Usage: warn view ", + "warn_ack_syntax": "Usage: warn ack ", + "warn_help_syntax": "Uwage: warn help ", "fwarn_add_syntax": "Usage: fwarn add [@] [~expiry] [sanctions] <:reason> [| notes]", "fwarn_page_invalid": "Invalid page, must be a number 1 or greater.", "fwarn_points_invalid": "Invalid points, must be a number above 0.", + "fwarn_expiry_invalid": "Invalid expiration amount, must be a number above 0.", "fwarn_expiry_invalid_suffix": "Invalid expiration suffix, must use either d, h, or m.", "fwarn_cannot_add": "Cannot add warning, double-check your parameters (the nick might be wrong or you are not joined to the channel).", "fwarn_added": "Added warning {0}.", @@ -813,24 +820,32 @@ "fwarn_stasis_invalid": "Invalid stasis amount, specify sanction as stasis=number.", "fwarn_deny_invalid": "Invalid denied commands, specify sanction as deny=command,command,command without spaces.", "fwarn_deny_invalid_command": "Invalid command \"{0}\", specify sanction as deny=command,command,command without spaces.", - "fwarn_list_header": "{0} has {1} active warning points.", + "fwarn_list_header": "{0} has {1} active warning points. Warnings prefixed with \u0002!\u0002 are unacknowledged.", + "warn_list_header": "You have {0} active warning points. You must acknowledge all warnings prefixed with \u0002!\u0002 by using \"warn ack \" before you can join games.", "fwarn_list": "{0}{1}[#{2} {3}] to {4} by {5} - {6} ({7} points, {8}){9}", + "warn_list": "{0}{1}[#{2} {3}] {4} ({5} points, {6}){7}", "fwarn_deleted": "deleted", "fwarn_expired": "expired", + "fwarn_list_expired": "expired on {0}", "fwarn_never_expires": "never expires", "fwarn_list_footer": "More results are available, use fwarn list {0} to view them.", + "warn_list_footer": "More results are available, use warn list {0} to view them.", "fwarn_list_empty": "No results.", "fwarn_invalid_warning": "The specified warning id does not exist or you do not have permission to view it.", "fwarn_view_header": "Warning #{0}, given to {1} on {2} by {3}. {4} points. {5}.", + "warn_view_header": "Warning #{0}, given on {1}. {2} points. {3}.", "fwarn_view_active": "Currently active, {0}", "fwarn_view_expires": "expires on {0}", "fwarn_view_expired": "Expired on {0}", "fwarn_view_deleted": "Deleted on {0} by {1}", "fwarn_view_ack": "Warning has not yet been acknowledged.", + "warn_view_ack": "You have not yet acknowledge this warning. You must acknowledge this warning by using \"warn ack {0}\" before you can join games.", "fwarn_view_sanctions": "Sanctions:", - "fwarn_view_stasis": "{0} games of stasis.", + "fwarn_view_stasis_sing": "1 game of stasis.", + "fwarn_view_stasis_plural": "{0} games of stasis.", "fwarn_view_deny": "denied {0}.", "fwarn_reason_required": "A public warning reason is required.", + "warn_unacked": "You have unacknowledged warnings and cannot join at this time. Use \"warn list\" to view them.", "_": " vim: set sw=4 expandtab:" } diff --git a/src/__init__.py b/src/__init__.py index ac53da1..f011911 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -84,10 +84,6 @@ if args.normal: normal = True botconfig.DEBUG_MODE = debug_mode if not normal else False botconfig.VERBOSE_MODE = verbose if not normal else False -# Initialize Database - -db.init() - # Logger # replace characters that can't be encoded with '?' diff --git a/src/db.py b/src/db.py index 38e4fc1..eae9dd5 100644 --- a/src/db.py +++ b/src/db.py @@ -3,34 +3,37 @@ import src.settings as var import sqlite3 import os import json +from collections import defaultdict # increment this whenever making a schema change so that the schema upgrade functions run on start # they do not run by default for performance reasons SCHEMA_VERSION = 1 -conn = None +need_install = not os.path.isfile("data.sqlite3") +conn = sqlite3.connect("data.sqlite3") +with conn: + c = conn.cursor() + c.execute("PRAGMA foreign_keys = ON") + if need_install: + _install() + c.execute("PRAGMA user_version") + row = c.fetchone() + if row[0] == 0: + # new schema does not exist yet, migrate from old schema + # NOTE: game stats are NOT migrated to the new schema; the old gamestats table + # will continue to exist to allow queries against it, however given how horribly + # inaccurate the stats on it are, it would be a disservice to copy those inaccurate + # statistics over to the new schema which has the capability of actually being accurate. + _migrate() + elif row[0] < SCHEMA_VERSION: + _upgrade() + c.close() -def init(): - global conn - need_install = not os.path.isfile("data.sqlite3") - conn = sqlite3.connect("data.sqlite3") - with conn: +del need_install, c + +def init_vars(): + with var.GRAVEYARD_LOCK: c = conn.cursor() - c.execute("PRAGMA foreign_keys = ON") - if need_install: - _install() - c.execute("PRAGMA user_version") - row = c.fetchone() - if row[0] == 0: - # new schema does not exist yet, migrate from old schema - # NOTE: game stats are NOT migrated to the new schema; the old gamestats table - # will continue to exist to allow queries against it, however given how horribly - # inaccurate the stats on it are, it would be a disservice to copy those inaccurate - # statistics over to the new schema which has the capability of actually being accurate. - _migrate() - elif row[0] < SCHEMA_VERSION: - _upgrade() - c.execute("""SELECT pl.account, pl.hostmask, @@ -39,14 +42,37 @@ def init(): pe.deadchat, pe.pingif, pe.stasis_amount, - pe.stasis_expires + pe.stasis_expires, + COALESCE(at.flags, a.flags) FROM person pe JOIN person_player pp ON pp.person = pe.id JOIN player pl ON pl.id = pp.player + LEFT JOIN access a + ON a.person = pe.id + LEFT JOIN access_template at + ON at.id = a.template WHERE pl.active = 1""") - for (acc, host, notice, simple, dc, pi, stasis, stasisexp) in c: + + var.SIMPLE_NOTIFY = set() # cloaks of people who !simple, who don't want detailed instructions + var.SIMPLE_NOTIFY_ACCS = set() # same as above, except accounts. takes precedence + var.PREFER_NOTICE = set() # cloaks of people who !notice, who want everything /notice'd + var.PREFER_NOTICE_ACCS = set() # Same as above, except accounts. takes precedence + var.STASISED = defaultdict(int) + var.STASISED_ACCS = defaultdict(int) + var.PING_IF_PREFS = {} + var.PING_IF_PREFS_ACCS = {} + var.PING_IF_NUMS = defaultdict(set) + var.PING_IF_NUMS_ACCS = defaultdict(set) + var.DEADCHAT_PREFS = set() + var.DEADCHAT_PREFS_ACCS = set() + var.FLAGS = defaultdict(str) + var.FLAGS_ACCS = defaultdict(str) + var.DENY = defaultdict(set) + var.DENY_ACCS = defaultdict(set) + + for acc, host, notice, simple, dc, pi, stasis, stasisexp, flags in c: if acc is not None: if simple == 1: var.SIMPLE_NOTIFY_ACCS.add(acc) @@ -59,6 +85,8 @@ def init(): var.PING_IF_NUMS_ACCS[pi].add(acc) if dc == 1: var.DEADCHAT_PREFS_ACCS.add(acc) + if flags: + var.FLAGS_ACCS[acc] = flags elif host is not None: if simple == 1: var.SIMPLE_NOTIFY.add(host) @@ -71,6 +99,74 @@ def init(): var.PING_IF_NUMS[pi].add(host) if dc == 1: var.DEADCHAT_PREFS.add(host) + if flags: + var.FLAGS[host] = flags + + c.execute("""SELECT + pl.account, + pl.hostmask, + ws.data + FROM warning w + JOIN warning_sanction ws + ON ws.warning = w.id + JOIN person pe + ON pe.id = w.target + JOIN person_player pp + ON pp.person = pe.id + JOIN player pl + ON pl.id = pp.player + WHERE + ws.sanction = 'deny command' + AND w.deleted = 0 + AND ( + w.expires IS NULL + OR w.expires > datetime('now') + )""") + for acc, host, command in c: + if acc is not None: + var.DENY_ACCS[acc].add(command) + if host is not None: + var.DENY[host].add(command) + +init_vars() + +def decrement_stasis(acc=None, hostmask=None): + peid, plid = _get_ids(acc, hostmask) + if (acc is not None or hostmask is not None) and peid is None: + return + sql = "UPDATE person SET stasis_amount = MAX(0, stasis_amount - 1)" + params = () + if peid is not None: + sql += " WHERE id = ?" + params = (peid,) + + with conn: + c = conn.cursor() + c.execute(sql, params) + +def decrease_stasis(newamt, acc=None, hostmask=None): + peid, plid = _get_ids(acc, hostmask) + if peid is None: + return + if newamt < 0: + newamt = 0 + + with conn: + c = conn.cursor() + c.execute("""UPDATE person + SET stasis_amount = MIN(stasis_amount, ?) + WHERE id = ?""", (newamt, peid)) + +def expire_stasis(): + with conn: + c = conn.cursor() + c.execute("""UPDATE person + SET + stasis_amount = 0, + stasis_expires = NULL + WHERE + stasis_expires IS NOT NULL + AND stasis_expires <= datetime('now')""") def toggle_simple(acc, hostmask): _toggle_thing("simple", acc, hostmask) @@ -84,9 +180,6 @@ def toggle_deadchat(acc, hostmask): def set_pingif(val, acc, hostmask): _set_thing("pingif", val, acc, hostmask, raw=False) -def set_stasis(val, acc, hostmask): - _set_thing("stasis_amount", val, acc, hostmask, raw=False) - def add_game(mode, size, started, finished, winner, players, options): """ Adds a game record to the database. @@ -257,39 +350,6 @@ def get_game_totals(mode): totals.append("\u0002{0}p\u0002: {1}".format(*row)) return "Total games ({0}) | {1}".format(total_games, ", ".join(totals)) -def get_flags(acc, hostmask): - peid, plid = _get_ids(acc, hostmask) - c = conn.cursor() - c.execute("""SELECT COALESCE(at.flags, a.flags) - FROM access a - LEFT JOIN access_template at - ON at.id = a.template - WHERE a.person = ?""", (peid,)) - row = c.fetchone() - if row is None: - return "" - return row[0] - -def get_denied_commands(acc, hostmask): - peid, plid = _get_ids(acc, hostmask) - c = conn.cursor() - c.execute("""SELECT ws.data - FROM warning w - JOIN warning_sanction ws - ON ws.warning = w.id - WHERE - ws.sanction = 'deny command' - AND w.target = ? - AND w.deleted = 0 - AND ( - w.expires IS NULL - OR w.expires > datetime('now') - )""", (peid,)) - cmds = set() - for row in c: - cmds.add(row[0]) - return cmds - def get_warning_points(acc, hostmask): peid, plid = _get_ids(acc, hostmask) c = conn.cursor() @@ -305,6 +365,23 @@ def get_warning_points(acc, hostmask): row = c.fetchone() return row[0] +def has_unacknowledged_warnings(acc, hostmask): + peid, plid = _get_ids(acc, hostmask) + if peid is None: + return False + c = conn.cursor() + c.execute("""SELECT MIN(acknowledged) + FROM warning + WHERE + target = ? + AND deleted = 0 + AND ( + expires IS NULL + OR expires > datetime('now') + )""", (peid,)) + row = c.fetchone() + return not bool(row[0]) + def list_all_warnings(list_all=False, skip=0, show=0): c = conn.cursor() sql = """SELECT @@ -356,7 +433,7 @@ def list_all_warnings(list_all=False, skip=0, show=0): "reason": row[9]}) return warnings -def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): +def list_warnings(acc, hostmask, expired=False, deleted=False, skip=0, show=0): peid, plid = _get_ids(acc, hostmask) c = conn.cursor() sql = """SELECT @@ -383,16 +460,16 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): WHERE warning.target = ? """ - if not list_all: - sql += """ AND deleted = 0 - AND ( + if not deleted: + sql += " AND deleted = 0" + if not expired: + sql += """ AND ( expires IS NULL OR expires > datetime('now') - ) - """ - sql += "ORDER BY warning.issued DESC\n" + )""" + sql += " ORDER BY warning.issued DESC" if show > 0: - sql += "LIMIT {0} OFFSET {1}".format(show, skip) + sql += " LIMIT {0} OFFSET {1}".format(show, skip) c.execute(sql, (botconfig.NICK, peid)) warnings = [] @@ -410,7 +487,7 @@ def list_warnings(acc, hostmask, list_all=False, skip=0, show=0): return warnings def get_warning(warn_id, acc=None, hm=None): - pe, pl = _get_ids(acc, hm) + peid, plid = _get_ids(acc, hm) c = conn.cursor() sql = """SELECT warning.id, @@ -486,13 +563,14 @@ def get_warning_sanctions(warn_id): def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack): teid, tlid = _get_ids(tacc, thm) seid, slid = _get_ids(sacc, shm) + ack = 0 if need_ack else 1 with conn: c = conn.cursor() c.execute("""INSERT INTO warning ( target, sender, amount, issued, expires, - reasons, notes, + reason, notes, acknowledged ) VALUES @@ -501,7 +579,7 @@ def add_warning(tacc, thm, sacc, shm, amount, reason, notes, expires, need_ack): datetime('now'), ?, ?, ?, ? - )""", (teid, seid, amount, expires, reasons, notes, not need_ack)) + )""", (teid, seid, amount, expires, reason, notes, ack)) return c.lastrowid def add_warning_sanction(warning, sanction, data): @@ -512,6 +590,19 @@ def add_warning_sanction(warning, sanction, data): VALUES (?, ?, ?)""", (warning, sanction, data)) + if sanction == "stasis": + c.execute("SELECT target FROM warning WHERE id = ?", (warning,)) + peid = c.fetchone()[0] + c.execute("""UPDATE person + SET + stasis_amount = stasis_amount + ?, + stasis_expires = datetime(CASE WHEN stasis_expires IS NULL + OR stasis_expires <= datetime('now') + THEN 'now' + ELSE stasis_expires END, + '+{0} hours') + WHERE id = ?""".format(int(data)), (data, peid)) + def del_warning(warning, acc, hm): peid, plid = _get_ids(acc, hm) with conn: @@ -533,6 +624,11 @@ def set_warning(warning, reason, notes): SET reason = ?, notes = ? WHERE id = ?""", (reason, notes, warning)) +def acknowledge_warning(warning): + with conn: + c = conn.cursor() + c.execute("UPDATE warning SET acknowledged = 1 WHERE id = ?", (warning,)) + def _upgrade(): # no upgrades yet, once there are some, add methods like _add_table(), _add_column(), etc. # that check for the existence of that table/column/whatever and adds/drops/whatevers them diff --git a/src/decorators.py b/src/decorators.py index 853b286..ac3af7d 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -188,15 +188,13 @@ class cmd: cli.notice(nick, messages["not_owner"]) return - # TODO: cache flags and cmds (below) on init, possibly store in var.USERS - # that would greatly reduce our db calls - flags = db.get_flags(acc, hostmask) + flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc] is_full_admin = "F" in flags if self.flag and (is_full_admin or is_owner): adminlog(chan, rawnick, self.name, rest) return self.func(*largs) - denied_cmds = db.get_denied_commands(acc, hostmask) + denied_cmds = var.DENY[hostmask] | var.DENY_ACCS[acc] for command in self.cmds: if command in denied_cmds: if chan == nick: diff --git a/src/settings.py b/src/settings.py index cdb0701..3376704 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,5 +1,6 @@ import fnmatch import re +import threading from collections import defaultdict, OrderedDict import botconfig @@ -195,10 +196,6 @@ TOTEM_CHANCES = { "death": ( 1 , 1 , 0 GAME_MODES = {} GAME_PHASES = ("night", "day") # all phases that constitute "in game", game modes can extend this with custom phases -SIMPLE_NOTIFY = set() # cloaks of people who !simple, who don't want detailed instructions -SIMPLE_NOTIFY_ACCS = set() # same as above, except accounts. takes precedence -PREFER_NOTICE = set() # cloaks of people who !notice, who want everything /notice'd -PREFER_NOTICE_ACCS = set() # Same as above, except accounts. takes precedence ACCOUNTS_ONLY = False # If True, will use only accounts for everything DISABLE_ACCOUNTS = False # If True, all account-related features are disabled. Automatically set if we discover we do not have proper ircd support for accounts @@ -212,9 +209,6 @@ NICKSERV_REGAIN_COMMAND = "REGAIN {nick}" CHANSERV = "ChanServ" CHANSERV_OP_COMMAND = "OP {channel}" -STASISED = defaultdict(int) -STASISED_ACCS = defaultdict(int) - # TODO: move this to a game mode called "fixed" once we implement a way to randomize roles (and have that game mode be called "random") DEFAULT_ROLE = "villager" ROLE_INDEX = ( 4 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 15 , 16 , 18 , 20 , 21 , 23 , 24 ) @@ -328,16 +322,9 @@ FORTUNE_CHANCE = 1/25 RULES = (botconfig.CHANNEL + " channel rules: http://wolf.xnrand.com/rules") -# pingif-related mappings - -PING_IF_PREFS = {} -PING_IF_PREFS_ACCS = {} - -PING_IF_NUMS = defaultdict(set) -PING_IF_NUMS_ACCS = defaultdict(set) - -DEADCHAT_PREFS = set() -DEADCHAT_PREFS_ACCS = set() +GRAVEYARD_LOCK = threading.RLock() +WARNING_LOCK = threading.RLock() +WAIT_TB_LOCK = threading.RLock() #TODO: move all of these to util.py or other files, as they are certainly NOT settings! @@ -386,7 +373,7 @@ def is_admin(nick, ident=None, host=None, acc=None): if not acc: acc = USERS[nick]["account"] hostmask = nick + "!" + ident + "@" + host - flags = db.get_flags(acc, hostmask) + flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc] return "F" in flags def irc_lower(nick): diff --git a/src/wolfgame.py b/src/wolfgame.py index ccc39fc..3495993 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -89,9 +89,6 @@ var.LAST_SAID_TIME = {} var.GAME_START_TIME = datetime.now() # for idle checker only var.CAN_START_TIME = 0 -var.GRAVEYARD_LOCK = threading.RLock() -var.WARNING_LOCK = threading.RLock() -var.WAIT_TB_LOCK = threading.RLock() var.STARTED_DAY_PLAYERS = 0 var.DISCONNECTED = {} # players who got disconnected @@ -361,6 +358,15 @@ def get_victim(cli, nick, victim, in_chan, self_in_list=False, bot_in_list=False return return pl[pll.index(tempvictim)] #convert back to normal casing +# wrapper around complete_match() used for any nick on the channel +def get_nick(cli, nick): + ul = [x for x in var.USERS] + ull = [x.lower() for x in var.USERS] + lnick, num_matches = complete_match(nick.lower(), ull) + if not lnick: + return None + return ul[ull.index(lnick)] + def get_roles(*roles): all_roles = [] for role in roles: @@ -449,6 +455,10 @@ def sync_modes(cli): mass_mode(cli, voices, other) +@cmd("refreshdb", flag="m", pm=True) +def refreshdb(cli, nick, chan, rest): + """Updates our tracking vars to the current db state.""" + db.init_vars() @cmd("fdie", "fbye", flag="D", pm=True) def forced_exit(cli, nick, chan, rest): @@ -1162,11 +1172,13 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): ident = var.USERS[player]["ident"] host = var.USERS[player]["host"] acc = var.USERS[player]["account"] + hostmask = player + "!" + ident + "@" + host elif is_fake_nick(player) and botconfig.DEBUG_MODE: # fakenick ident = None host = None acc = None + hostmask = None else: return False # Not normal if not acc or acc == "*" or var.DISABLE_ACCOUNTS: @@ -1183,6 +1195,11 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): "s" if stasis != 1 else "")) return False + # don't check unacked warnings on fjoin + if who == player and db.has_unacknowledged_warnings(acc, hostmask): + cli.notice(player, messages["warn_unacked"]) + return False + cmodes = [("+v", player)] if var.PHASE == "none": if var.AUTO_TOGGLE_MODES and player in var.USERS and var.USERS[player]["modes"]: @@ -1297,6 +1314,9 @@ def kill_join(cli, chan): reset() cli.msg(chan, msg) cli.msg(chan, messages["game_idle_cancel"]) + # use this opportunity to expire pending stasis + db.expire_stasis() + db.init_vars() if var.AFTER_FLASTGAME is not None: var.AFTER_FLASTGAME() var.AFTER_FLASTGAME = None @@ -7907,19 +7927,15 @@ def decrement_stasis(nick=None): acc = var.USERS[nick]["account"] # decrement account stasis even if accounts are disabled if acc in var.STASISED_ACCS: - var.STASISED_ACCS[acc] = max(0, var.STASISED_ACCS[acc] - 1) - db.set_stasis(var.STASISED_ACCS[acc], acc, None) + db.decrement_stasis(acc=acc) for hostmask in var.STASISED: if var.match_hostmask(hostmask, nick, ident, host): - var.STASISED[hostmask] = max(0, var.STASISED[hostmask] - 1) - db.set_stasis(var.STASISED[hostmask], None, hostmask) + db.decrement_stasis(hostmask=hostmask) else: - for acc in var.STASISED_ACCS: - var.STASISED_ACCS[acc] = max(0, var.STASISED_ACCS[acc] - 1) - db.set_stasis(var.STASISED_ACCS[acc], acc, None) - for hostmask in var.STASISED: - var.STASISED[hostmask] = max(0, var.STASISED[hostmask] - 1) - db.set_stasis(var.STASISED[hostmask], None, hostmask) + db.decrement_stasis() + # Also expire any expired stasis and update our tracking vars + db.expire_stasis() + db.init_vars() def parse_warning_target(target): if target[0] == "=": @@ -7988,6 +8004,9 @@ def add_warning(target, amount, actor, reason, notes=None, expires=None, need_ac for cmd in sanctions["deny"]: db.add_warning_sanction(sid, "deny command", cmd) + # Update any tracking vars that may have changed due to this + db.init_vars() + return sid @cmd("stasis", chan=True, pm=True) @@ -8000,14 +8019,226 @@ def stasis(cli, nick, chan, rest): reply(cli, nick, chan, msg, prefix_nick=True) +@cmd("fstasis", flag="A", chan=True, pm=True) +def fstasis(cli, nick, chan, rest): + """Removes or views stasis penalties.""" + + data = rest.split() + msg = None + + if data: + lusers = {k.lower(): v for k, v in var.USERS.items()} + acc, hostmask = parse_warning_target(data[0]) + cur = max(var.STASISED[hostmask], var.STASISED_ACCS[acc]) + + if len(data) == 1: + if acc is not None and var.STASISED_ACCS[acc] == cur: + plural = "" if cur == 1 else "s" + reply(cli, nick, chan, messages["account_in_stasis"].format(data[0], acc, cur, plural)) + elif hostmask is not None and var.STASISED[hostmask] == cur: + plural = "" if cur == 1 else "s" + reply(cli, nick, chan, messages["hostmask_in_stasis"].format(data[0], hostmask, cur, plural)) + elif acc is not None: + reply(cli, nick, chan, messages["account_not_in_stasis"].format(data[0], acc)) + else: + reply(cli, nick, chan, messages["hostmask_not_in_stasis"].format(data[0], hostmask)) + else: + try: + amt = int(data[1]) + except ValueError: + reply(cli, nick, chan, messages["stasis_not_negative"]) + return + + if amt < 0: + reply(cli, nick, chan, messages["stasis_not_negative"]) + return + elif amt > cur: + reply(cli, nick, chan, messages["stasis_cannot_increase"]) + return + elif cur == 0: + if acc is not None: + reply(cli, nick, chan, messages["account_not_in_stasis"].format(data[0], acc)) + return + else: + reply(cli, nick, chan, messages["hostmask_not_in_stasis"].format(data[0], hostmask)) + return + + db.decrease_stasis(amt, acc, hostmask) + db.init_vars() + if amt > 0: + plural = "" if amt == 1 else "s" + if acc is not None: + reply(cli, nick, chan, messages["fstasis_account_add"].format(data[0], acc, amt, plural)) + else: + reply(cli, nick, chan, messages["fstasis_hostmask_add"].format(data[0], hostmask, amt, plural)) + elif acc is not None: + reply(cli, nick, chan, messages["fstasis_account_remove"].format(data[0], acc)) + else: + reply(cli, nick, chan, messages["fstasis_hostmask_remove"].format(data[0], hostmask)) + elif var.STASISED or var.STASISED_ACCS: + stasised = {} + for hostmask in var.STASISED: + if var.DISABLE_ACCOUNTS: + stasised[hostmask] = var.STASISED[hostmask] + else: + stasised[hostmask+" (Host)"] = var.STASISED[hostmask] + if not var.DISABLE_ACCOUNTS: + for acc in var.STASISED_ACCS: + stasised[acc+" (Account)"] = var.STASISED_ACCS[acc] + msg = messages["currently_stasised"].format(", ".join( + "\u0002{0}\u0002 ({1})".format(usr, number) + for usr, number in stasised.items())) + reply(cli, nick, chan, msg) + else: + reply(cli, nick, chan, messages["noone_stasised"]) + @cmd("warn", pm=True) def warn(cli, nick, chan, rest): """View and acknowledge your warnings.""" - # !warn list [all] - lists all active warnings, or all warnings if all passed + # !warn list [-all] [page] - lists all active warnings, or all warnings if all passed # !warn view - views details on warning id # !warn ack - acknowledges warning id # Default if only !warn is given is to do !warn list. - pass + params = re.split(" +", rest) + + try: + command = params.pop(0) + if command == "": + command = "list" + except IndexError: + command = "list" + + if command not in ("list", "view", "ack", "help"): + reply(cli, nick, chan, messages["warn_usage"]) + return + + if command == "help": + try: + subcommand = params.pop(0) + except IndexError: + reply(cli, nick, chan, messages["warn_help_syntax"]) + return + if subcommand not in ("list", "view", "ack", "help"): + reply(cli, nick, chan, messages["warn_usage"]) + return + reply(cli, nick, chan, messages["warn_{0}_syntax".format(subcommand)]) + return + + if command == "list": + list_all = False + page = 1 + try: + list_all = params.pop(0) + target = params.pop(0) + page = int(params.pop(0)) + except IndexError: + pass + except ValueError: + reply(cli, nick, chan, messages["fwarn_page_invalid"]) + return + + try: + if list_all and list_all != "-all": + page = int(list_all) + list_all = False + elif list_all == "-all": + list_all = True + except ValueError: + reply(cli, nick, chan, messages["fwarn_page_invalid"]) + return + + acc, hm = parse_warning_target(nick) + warnings = db.list_warnings(acc, hm, expired=list_all, skip=(page-1)*10, show=11) + points = db.get_warning_points(acc, hm) + cli.notice(nick, messages["warn_list_header"].format(points)) + + i = 0 + for warn in warnings: + i += 1 + if (i == 11): + parts = [] + if list_all: + parts.append("-all") + parts.append(str(page + 1)) + cli.notice(nick, messages["warn_list_footer"].format(" ".join(parts))) + break + start = "" + end = "" + ack = "" + if warn["expires"] is not None: + if warn["expired"]: + expires = messages["fwarn_list_expired"].format(warn["expires"]) + else: + expires = messages["fwarn_view_expires"].format(warn["expires"]) + else: + expires = messages["fwarn_never_expires"] + if warn["expired"]: + start = "\u000314" + end = " [\u00037{0}\u000314]\u0003".format(messages["fwarn_expired"]) + if not warn["ack"]: + ack = "\u0002!\u0002 " + cli.notice(nick, messages["warn_list"].format( + start, ack, warn["id"], warn["issued"], warn["reason"], warn["amount"], expires, end)) + if i == 0: + cli.notice(nick, messages["fwarn_list_empty"]) + return + + if command == "view": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["warn_view_syntax"]) + return + + acc, hm = parse_warning_target(nick) + warning = db.get_warning(warn_id, acc, hm) + if warning is None: + reply(cli, nick, chan, messages["fwarn_invalid_warning"]) + return + + if warning["expired"]: + expires = messages["fwarn_view_expired"].format(warning["expires"]) + elif warning["expires"] is None: + expires = messages["fwarn_view_active"].format(messages["fwarn_never_expires"]) + else: + expires = messages["fwarn_view_active"].format(messages["fwarn_view_expires"].format(warning["expires"])) + + cli.notice(nick, messages["warn_view_header"].format( + warning["id"], warning["issued"], warning["amount"], expires)) + cli.notice(nick, warning["reason"]) + + sanctions = [] + if not warning["ack"]: + sanctions.append(messages["warn_view_ack"].format(warning["id"])) + if warning["sanctions"]: + sanctions.append(messages["fwarn_view_sanctions"]) + if "stasis" in warning["sanctions"]: + if warning["sanctions"]["stasis"] != 1: + sanctions.append(messages["fwarn_view_stasis_plural"].format(warning["sanctions"]["stasis"])) + else: + sanctions.append(messages["fwarn_view_stasis_sing"]) + if "deny" in warning["sanctions"]: + sanctions.append(messages["fwarn_view_deny"].format(", ".join(warning["sanctions"]["deny"]))) + if sanctions: + cli.notice(nick, " ".join(sanctions)) + return + + if command == "ack": + try: + warn_id = int(params.pop(0)) + except (IndexError, ValueError): + reply(cli, nick, chan, messages["warn_ack_syntax"]) + return + + acc, hm = parse_warning_target(nick) + warning = db.get_warning(warn_id, acc, hm) + if warning is None: + reply(cli, nick, chan, messages["fwarn_invalid_warning"]) + return + + db.acknowledge_warning(warn_id) + reply(cli, nick, chan, messages["fwarn_done"]) + return @cmd("fwarn", flag="A", pm=True) def fwarn(cli, nick, chan, rest): @@ -8039,7 +8270,7 @@ def fwarn(cli, nick, chan, rest): target = None points = None need_ack = False - expiry = None + expires = None sanctions = {} reason = None notes = None @@ -8050,6 +8281,10 @@ def fwarn(cli, nick, chan, rest): reply(cli, nick, chan, messages["fwarn_usage"]) return + if command not in ("list", "view", "add", "del", "set", "help"): + reply(cli, nick, chan, messages["fwarn_usage"]) + return + if command == "help": try: subcommand = params.pop(0) @@ -8069,18 +8304,30 @@ def fwarn(cli, nick, chan, rest): list_all = params.pop(0) target = params.pop(0) page = int(params.pop(0)) - if list_all and list_all != "-all": - if target is not None: - page = int(target) - target = list_all - list_all = False - elif show_all == "-all": - list_all = True except IndexError: pass except ValueError: reply(cli, nick, chan, messages["fwarn_page_invalid"]) return + + try: + if list_all and list_all != "-all": + if target is not None: + page = int(target) + target = list_all + list_all = False + elif list_all == "-all": + list_all = True + except ValueError: + reply(cli, nick, chan, messages["fwarn_page_invalid"]) + return + + try: + page = int(target) + target = None + except (TypeError, ValueError): + pass + if target is not None: acc, hm = parse_warning_target(target) if acc is None and hm is None: @@ -8088,7 +8335,7 @@ def fwarn(cli, nick, chan, rest): return warnings = db.list_warnings(acc, hm, list_all=list_all, skip=(page-1)*10, show=11) points = db.get_warning_points(acc, hm) - reply(cli, nick, chan, messages["fwarn_list_header"].format(target, points)) + cli.notice(nick, messages["fwarn_list_header"].format(target, points)) else: warnings = db.list_all_warnings(list_all=list_all, skip=(page-1)*10, show=11) @@ -8102,12 +8349,18 @@ def fwarn(cli, nick, chan, rest): if target is not None: parts.append(target) parts.append(str(page + 1)) - reply(cli, nick, chan, messages["fwarn_list_footer"].format(" ".join(parts))) + cli.notice(nick, messages["fwarn_list_footer"].format(" ".join(parts))) break start = "" end = "" ack = "" - expires = warn["expires"] if warn["expires"] is not None else messages["fwarn_never_expires"] + if warn["expires"] is not None: + if warn["expired"]: + expires = messages["fwarn_list_expired"].format(warn["expires"]) + else: + expires = messages["fwarn_view_expires"].format(warn["expires"]) + else: + expires = messages["fwarn_never_expires"] if warn["deleted"]: start = "\u000314" end = " [\u00034{0}\u000314]\u0003".format(messages["fwarn_deleted"]) @@ -8116,11 +8369,11 @@ def fwarn(cli, nick, chan, rest): end = " [\u00037{0}\u000314]\u0003".format(messages["fwarn_expired"]) if not warn["ack"]: ack = "\u0002!\u0002 " - reply(cli, nick, chan, messages["fwarn_list"].format( + cli.notice(nick, messages["fwarn_list"].format( start, ack, warn["id"], warn["issued"], warn["target"], warn["sender"], warn["reason"], warn["amount"], expires, end)) if i == 0: - reply(cli, nick, chan, messages["fwarn_list_empty"]) + cli.notice(nick, messages["fwarn_list_empty"]) return if command == "view": @@ -8144,14 +8397,14 @@ def fwarn(cli, nick, chan, rest): else: expires = messages["fwarn_view_active"].format(messages["fwarn_view_expires"].format(warning["expires"])) - reply(cli, nick, chan, messages["fwarn_view_header"].format( + cli.notice(nick, messages["fwarn_view_header"].format( warning["id"], warning["target"], warning["issued"], warning["sender"], warning["amount"], expires)) reason = [warning["reason"]] if warning["notes"] is not None: reason.append(warning["notes"]) - reply(cli, nick, chan, " | ".join(reason)) + cli.notice(nick, " | ".join(reason)) sanctions = [] if not warning["ack"]: @@ -8159,11 +8412,14 @@ def fwarn(cli, nick, chan, rest): if warning["sanctions"]: sanctions.append(messages["fwarn_view_sanctions"]) if "stasis" in warning["sanctions"]: - sanctions.append(messages["fwarn_view_stasis"].format(warning["sanctions"]["stasis"])) + if warning["sanctions"]["stasis"] != 1: + sanctions.append(messages["fwarn_view_stasis_plural"].format(warning["sanctions"]["stasis"])) + else: + sanctions.append(messages["fwarn_view_stasis_sing"]) if "deny" in warning["sanctions"]: sanctions.append(messages["fwarn_view_deny"].format(", ".join(warning["sanctions"]["deny"]))) if sanctions: - reply(cli, nick, chan, " ".join(sanctions)) + cli.notice(nick, " ".join(sanctions)) return if command == "del": @@ -8217,10 +8473,6 @@ def fwarn(cli, nick, chan, rest): reply(cli, nick, chan, messages["fwarn_done"]) return - if command != "add": - reply(cli, nick, chan, messages["fwarn_usage"]) - return - # command == "add" while params: p = params.pop(0) @@ -8256,7 +8508,7 @@ def fwarn(cli, nick, chan, rest): if p == "~": reply(cli, nick, chan, messages["fwarn_syntax"]) return - expiry = p[1:] + expires = p[1:] else: # sanctions are the only thing left here sanc = p.split("=", 1) @@ -8303,12 +8555,22 @@ def fwarn(cli, nick, chan, rest): # convert expires into a proper datetime if expires is not None: suffix = expires[-1] + try: + amount = int(expires[:-1]) + except ValueError: + reply(cli, nick, chan, messages["fwarn_expiry_invalid"]) + return + + if amount <= 0: + reply(cli, nick, chan, messages["fwarn_expiry_invalid"]) + return + if suffix == "d": - expires = datetime.now() + timedelta(days=expires[:-1]) + expires = datetime.now() + timedelta(days=amount) elif suffix == "h": - expires = datetime.now() + timedelta(hours=expires[:-1]) + expires = datetime.now() + timedelta(hours=amount) elif suffix == "m": - expires = datetime.now() + timedelta(minutes=expires[:-1]) + expires = datetime.now() + timedelta(minutes=amount) else: reply(cli, nick, chan, messages["fwarn_expiry_invalid_suffix"]) return From 2151df4fefe98e0c0ff37f2d552e9da7a3f4137f Mon Sep 17 00:00:00 2001 From: skizzerz Date: Tue, 7 Jun 2016 17:02:12 -0500 Subject: [PATCH 04/10] Add !fflags and !ftemplate These allow for access control, and work pretty much exactly like they do in atheme's ChanServ (the /cs flags and /cs template commands). Also remove unused things from botconfig.py.example. --- botconfig.py.example | 4 -- messages/en.json | 13 ++++ src/db.py | 50 ++++++++++++++ src/settings.py | 1 + src/wolfgame.py | 161 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 4 deletions(-) diff --git a/botconfig.py.example b/botconfig.py.example index 8102987..f289399 100644 --- a/botconfig.py.example +++ b/botconfig.py.example @@ -22,11 +22,7 @@ CMD_CHAR = "!" SERVER_PASS = "{account}:{password}" OWNERS = ("unaffiliated/wolfbot_admin1",) # The comma is required at the end if there is only one owner. -ADMINS = ("unaffiliated/wolfbot_admin2", "unaffiliated/wolfbot_test*") - OWNERS_ACCOUNTS = ("1owner_acc",) -ADMINS_ACCOUNTS = ("1admin_acc", "2admin_acc") - ALLOWED_NORMAL_MODE_COMMANDS = [] # Debug mode commands to be allowed in normal mode OWNERS_ONLY_COMMANDS = [] # Commands that should only be allowed for owners, regardless of their original permissions diff --git a/messages/en.json b/messages/en.json index 1f0060e..1220193 100644 --- a/messages/en.json +++ b/messages/en.json @@ -846,6 +846,19 @@ "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.", + "no_templates": "There are no access templates defined.", + "template_not_found": "There is no template named {0}.", + "template_set": "Set template {0} to flags +{1}.", + "template_deleted": "Removed template {0}. Any access entries using this template have also been deleted.", + "access_set_account": "Set access for account {0} to +{1}.", + "access_set_host": "Set access for host {0} to +{1}.", + "access_deleted_account": "Deleted access for account {0}.", + "access_deleted_host": "Deleted access for host {0}.", + "invalid_flag": "Invalid flag {0}. Valid flags are +{1}.", + "no_access_account": "Account {0} does not have any access.", + "access_account": "Account {0} has access +{1}.", + "no_access_host": "Host {0} does not have any access.", + "access_host": "Host {0} has access +{1}.", "_": " vim: set sw=4 expandtab:" } diff --git a/src/db.py b/src/db.py index eae9dd5..9b1f208 100644 --- a/src/db.py +++ b/src/db.py @@ -168,6 +168,56 @@ def expire_stasis(): stasis_expires IS NOT NULL AND stasis_expires <= datetime('now')""") +def get_template(name): + c = conn.cursor() + c.execute("SELECT id, flags FROM access_template WHERE name = ?", (name,)) + row = c.fetchone() + if row is None: + return (None, set()) + return (row[0], row[1]) + +def get_templates(): + c = conn.cursor() + c.execute("SELECT name, flags FROM access_template ORDER BY name ASC") + tpls = [] + for name, flags in c: + tpls.append((name, flags)) + return tpls + +def update_template(name, flags): + with conn: + tid, _ = get_template(name) + c = conn.cursor() + if tid is None: + c.execute("INSERT INTO access_template (name, flags) VALUES (?, ?)", (name, flags)) + else: + c.execute("UPDATE access_template SET flags = ? WHERE id = ?", (flags, tid)) + +def delete_template(name): + with conn: + tid, _ = get_template(name) + if tid is not None: + c = conn.cursor() + c.execute("DELETE FROM access WHERE template = ?", (tid,)) + c.execute("DELETE FROM template WHERE id = ?", (tid,)) + +def set_access(acc, hostmask, flags=None, tid=None): + peid, plid = _get_ids(acc, hostmask) + if peid is None: + return + with conn: + c = conn.cursor() + if flags is None and tid is None: + c.execute("DELETE FROM access WHERE person = ?", (peid,)) + elif tid is not None: + c.execute("""INSERT OR REPLACE INTO access + (person, template, flags) + VALUES (?, ?, NULL)""", (peid, tid)) + else: + c.execute("""INSERT OR REPLACE INTO access + (person, template, flags) + VALUES (?, NULL, ?)""", (peid, flags)) + def toggle_simple(acc, hostmask): _toggle_thing("simple", acc, hostmask) diff --git a/src/settings.py b/src/settings.py index 3376704..a41a28d 100644 --- a/src/settings.py +++ b/src/settings.py @@ -319,6 +319,7 @@ DISABLED_ROLES = frozenset() GIF_CHANCE = 1/50 FORTUNE_CHANCE = 1/25 +ALL_FLAGS = frozenset("AaDdFjms") RULES = (botconfig.CHANNEL + " channel rules: http://wolf.xnrand.com/rules") diff --git a/src/wolfgame.py b/src/wolfgame.py index 3495993..f78a232 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -8581,6 +8581,167 @@ def fwarn(cli, nick, chan, rest): else: reply(cli, nick, chan, messages["fwarn_added"].format(warn_id)) +@cmd("ftemplate", "F", pm=True) +def ftemplate(cli, nick, chan, rest): + params = re.split(" +", rest) + + if params[0] == "": + # display a list of all templates + tpls = db.get_templates() + if not tpls: + reply(cli, nick, chan, messages["no_templates"]) + else: + tpls = ["{0} (+{1})".format(name, "".join(sorted(flags))) for name, flags in tpls] + reply(cli, nick, chan, var.break_long_message(tpls, ", ")) + elif len(params) == 1: + reply(cli, nick, chan, messages["not_enough_parameters"]) + else: + name = params[0].upper() + flags = params[1] + tid, cur_flags = db.get_template(name) + + if flags[0] != "+" and flags[0] != "-": + # flags is a template name + tpl_name = flags.upper() + tpl_id, tpl_flags = db.get_template(tpl_name) + if tpl_id is None: + reply(cli, nick, chan, messages["template_not_found"].format(tpl_name)) + return + tpl_flags = "".join(sorted(tpl_flags)) + db.update_template(name, tpl_flags) + reply(cli, nick, chan, messages["template_set"].format(name, tpl_flags)) + else: + adding = True + for flag in flags: + if flag == "+": + adding = True + continue + elif flag == "-": + adding = False + continue + elif flag == "*": + if adding: + cur_flags = cur_flags | (var.ALL_FLAGS - {"F"}) + else: + cur_flags = set() + continue + elif flag not in var.ALL_FLAGS: + reply(cli, nick, chan, messages["invalid_flag"].format(flag, "".join(sorted(var.ALL_FLAGS)))) + return + elif adding: + cur_flags.add(flag) + else: + cur_flags.discard(flag) + if cur_flags: + tpl_flags = "".join(sorted(cur_flags)) + db.update_template(name, tpl_flags) + reply(cli, nick, chan, messages["template_set"].format(name, tpl_flags)) + elif tid is None: + reply(cli, nick, chan, messages["template_not_found"].format(name)) + else: + db.delete_template(name) + reply(cli, nick, chan, messages["template_deleted"].format(name)) + + # re-init var.FLAGS and var.FLAGS_ACCS since they may have changed + db.init_vars() + +@cmd("fflags", flag="F", pm=True) +def fflags(cli, nick, chan, rest): + params = re.split(" +", rest) + + if params[0] == "": + # display a list of all access + parts = [] + for acc, flags in var.FLAGS_ACCS.items(): + if not flags: + continue + if var.ACCOUNTS_ONLY: + parts.append("{0} (+{1})".format(acc, "".join(sorted(flags)))) + else: + parts.append("{0} (Account) (+{1})".format(acc, "".join(sorted(flags)))) + for hm, flags in var.FLAGS.items(): + if not flags: + continue + if var.DISABLE_ACCOUNTS: + parts.append("{0} (+{1})".format(hm, "".join(sorted(flags)))) + else: + parts.append("{0} (Host) (+{1})".format(hm, "".join(sorted(flags)))) + if not parts: + reply(cli, nick, chan, messages["no_access"]) + else: + reply(cli, nick, chan, var.break_long_message(parts, ", ")) + elif len(params) == 1: + # display access for the given user + acc, hm = parse_warning_target(params[0]) + if acc is not None: + if not var.FLAGS_ACCS[acc]: + msg = messages["no_access_account"].format(acc) + else: + msg = messages["access_account"].format(acc, "".join(sorted(var.FLAGS_ACCS[acc]))) + elif hm is not None: + if not var.FLAGS[hm]: + msg = messages["no_access_host"].format(hm) + else: + msg = messages["access_host"].format(acc, "".join(sorted(var.FLAGS[hm]))) + reply(cli, nick, chan, msg) + else: + acc, hm = parse_warning_target(params[0]) + flags = params[1] + cur_flags = set(var.FLAGS_ACCS[acc] + var.FLAGS[hm]) + + if flags[0] != "+" and flags[0] != "-": + # flags is a template name + tpl_name = flags.upper() + tpl_id, tpl_flags = db.get_template(tpl_name) + if tpl_id is None: + reply(cli, nick, chan, messages["template_not_found"].format(tpl_name)) + return + tpl_flags = "".join(sorted(tpl_flags)) + db.set_access(acc, hm, tid=tpl_id) + if acc is not None: + reply(cli, nick, chan, messages["access_set_account"].format(acc, tpl_flags)) + else: + reply(cli, nick, chan, messages["access_set_host"].format(hm, tpl_flags)) + else: + adding = True + for flag in flags: + if flag == "+": + adding = True + continue + elif flag == "-": + adding = False + continue + elif flag == "*": + if adding: + cur_flags = cur_flags | (var.ALL_FLAGS - {"F"}) + else: + cur_flags = set() + continue + elif flag not in var.ALL_FLAGS: + reply(cli, nick, chan, messages["invalid_flag"].format(flag, "".join(sorted(var.ALL_FLAGS)))) + return + elif adding: + cur_flags.add(flag) + else: + cur_flags.discard(flag) + if cur_flags: + flags = "".join(sorted(cur_flags)) + db.set_access(acc, hm, flags=flags) + if acc is not None: + reply(cli, nick, chan, messages["access_set_account"].format(acc, flags)) + else: + reply(cli, nick, chan, messages["access_set_host"].format(hm, flags)) + else: + db.set_access(acc, hm, flags=None) + if acc is not None: + reply(cli, nick, chan, messages["access_deleted_account"].format(acc)) + else: + reply(cli, nick, chan, messages["access_deleted_host"].format(hm)) + + # re-init var.FLAGS and var.FLAGS_ACCS since they may have changed + db.init_vars() + + @cmd("wait", "w", playing=True, phases=("join",)) def wait(cli, nick, chan, rest): """Increases the wait time until !start can be used.""" From adea98e3ed4f38fcb91aa2fd70e1b6126369d99b Mon Sep 17 00:00:00 2001 From: skizzerz Date: Sun, 12 Jun 2016 23:03:47 -0500 Subject: [PATCH 05/10] Fix some derps Need to move module init code after the functions it calls, also forgot the sanction definitions for 11-14 points. --- src/db.py | 47 +++++++++++++++++++++++------------------------ src/settings.py | 1 + 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/db.py b/src/db.py index 9b1f208..dd7c763 100644 --- a/src/db.py +++ b/src/db.py @@ -9,28 +9,6 @@ from collections import defaultdict # they do not run by default for performance reasons SCHEMA_VERSION = 1 -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() - -del need_install, c - def init_vars(): with var.GRAVEYARD_LOCK: c = conn.cursor() @@ -128,8 +106,6 @@ def init_vars(): 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: @@ -795,4 +771,27 @@ def _set_thing(thing, val, acc, hostmask, raw=False): def _toggle_thing(thing, acc, hostmask): _set_thing(thing, "CASE {0} WHEN 1 THEN 0 ELSE 1 END".format(thing), acc, hostmask, raw=True) +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() + +del need_install, c +init_vars() + # vim: set expandtab:sw=4:ts=4: diff --git a/src/settings.py b/src/settings.py index a41a28d..aa2131f 100644 --- a/src/settings.py +++ b/src/settings.py @@ -82,6 +82,7 @@ AUTO_SANCTION = ( (1, 4, {"ack": True}), (5, 9, {"stasis": 1}), (10, 10, {"ack": True, "stasis": 3}), + (11, 14, {"stasis": 3}), (15, 24, {"scalestasis": (0, 1, -10)}), (25, 25, {"tempban": 15}) ) From 2f62c4a8674b5497ab2624599a41e953c9801d48 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Mon, 13 Jun 2016 16:40:57 -0500 Subject: [PATCH 06/10] Fix some embarassing typos --- src/settings.py | 2 +- src/wolfgame.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/settings.py b/src/settings.py index aa2131f..8515625 100644 --- a/src/settings.py +++ b/src/settings.py @@ -375,7 +375,7 @@ def is_admin(nick, ident=None, host=None, acc=None): if not acc: acc = USERS[nick]["account"] hostmask = nick + "!" + ident + "@" + host - flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc] + flags = FLAGS[hostmask] + FLAGS_ACCS[acc] return "F" in flags def irc_lower(nick): diff --git a/src/wolfgame.py b/src/wolfgame.py index f78a232..542f815 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -8575,7 +8575,7 @@ def fwarn(cli, nick, chan, rest): reply(cli, nick, chan, messages["fwarn_expiry_invalid_suffix"]) return - warn_id = add_warning(target, amount, nick, reason, notes, expires, need_ack, sanctions) + warn_id = add_warning(target, points, nick, reason, notes, expires, need_ack, sanctions) if warn_id is False: reply(cli, nick, chan, messages["fwarn_cannot_add"]) else: From b39828be7f2b4f2f9555b2cee2e3f53c8a964104 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Tue, 14 Jun 2016 12:02:54 -0500 Subject: [PATCH 07/10] Remove !join bug. Now it should actually work even if the user has no warnings. Also don't list expired/deleted warnings as unacknowledged. Warning expiration can now be modified via !fwarn set, and the default expiration is now 30d instead of never (never can be manually specified along with a handful of aliases that mean the same thing). --- messages/en.json | 3 ++- src/db.py | 22 ++++++++++++++------ src/settings.py | 1 + src/wolfgame.py | 52 ++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/messages/en.json b/messages/en.json index 1220193..739b9bf 100644 --- a/messages/en.json +++ b/messages/en.json @@ -811,7 +811,7 @@ "fwarn_add_syntax": "Usage: fwarn add [@] [~expiry] [sanctions] <:reason> [| notes]", "fwarn_page_invalid": "Invalid page, must be a number 1 or greater.", "fwarn_points_invalid": "Invalid points, must be a number above 0.", - "fwarn_expiry_invalid": "Invalid expiration amount, must be a number above 0.", + "fwarn_expiry_invalid": "Invalid expiration amount, must be a number above 0 or 'never' for a warning that never expires.", "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}.", @@ -859,6 +859,7 @@ "access_account": "Account {0} has access +{1}.", "no_access_host": "Host {0} does not have any access.", "access_host": "Host {0} has access +{1}.", + "never_aliases": ["never", "infinite", "infinity", "permanent", "p"], "_": " vim: set sw=4 expandtab:" } diff --git a/src/db.py b/src/db.py index dd7c763..009f0ac 100644 --- a/src/db.py +++ b/src/db.py @@ -396,7 +396,7 @@ def has_unacknowledged_warnings(acc, hostmask): if peid is None: return False c = conn.cursor() - c.execute("""SELECT MIN(acknowledged) + c.execute("""SELECT COALESCE(MIN(acknowledged), 1) FROM warning WHERE target = ? @@ -419,7 +419,12 @@ def list_all_warnings(list_all=False, skip=0, show=0): warning.expires, CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') THEN 0 ELSE 1 END AS expired, - warning.acknowledged, + CASE WHEN warning.deleted + OR ( + warning.expires IS NOT NULL + AND warning.expires <= datetime('now') + ) + THEN 1 ELSE warning.acknowledged END AS acknowledged, warning.deleted, warning.reason FROM warning @@ -471,7 +476,12 @@ def list_warnings(acc, hostmask, expired=False, deleted=False, skip=0, show=0): warning.expires, CASE WHEN warning.expires IS NULL OR warning.expires > datetime('now') THEN 0 ELSE 1 END AS expired, - warning.acknowledged, + CASE WHEN warning.deleted + OR ( + warning.expires IS NOT NULL + AND warning.expires <= datetime('now') + ) + THEN 1 ELSE warning.acknowledged END AS acknowledged, warning.deleted, warning.reason FROM warning @@ -643,12 +653,12 @@ def del_warning(warning, acc, hm): id = ? AND deleted = 0""", (peid, warning)) -def set_warning(warning, reason, notes): +def set_warning(warning, expires, reason, notes): with conn: c = conn.cursor() c.execute("""UPDATE warning - SET reason = ?, notes = ? - WHERE id = ?""", (reason, notes, warning)) + SET reason = ?, notes = ?, expires = ? + WHERE id = ?""", (reason, notes, expires, warning)) def acknowledge_warning(warning): with conn: diff --git a/src/settings.py b/src/settings.py index 8515625..61a4c08 100644 --- a/src/settings.py +++ b/src/settings.py @@ -60,6 +60,7 @@ QUIET_PREFIX = "" # "" or "~q:" # The bot will automatically toggle those modes of people joining AUTO_TOGGLE_MODES = "" +DEFAULT_EXPIRY = "30d" LEAVE_PENALTY = 1 LEAVE_EXPIRY = "30d" IDLE_PENALTY = 1 diff --git a/src/wolfgame.py b/src/wolfgame.py index 542f815..fcaeb8c 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -8443,7 +8443,7 @@ def fwarn(cli, nick, chan, rest): try: warn_id = int(params.pop(0)) except (IndexError, ValueError): - reply(cli, nick, chan, messages["fwarn_del_syntax"]) + reply(cli, nick, chan, messages["fwarn_set_syntax"]) return warning = db.get_warning(warn_id) @@ -8455,11 +8455,46 @@ def fwarn(cli, nick, chan, rest): 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_suffix"]) + return + + # maintain existing reason if none was specified if not reason: - reply(cli, nick, chan, messages["fwarn_reason_required"]) - return + reason = warning["reason"] # maintain existing notes if none were specified if notes is not None: @@ -8469,7 +8504,7 @@ def fwarn(cli, nick, chan, rest): else: notes = warning["notes"] - db.set_warning(warn_id, reason, notes) + db.set_warning(warn_id, expires, reason, notes) reply(cli, nick, chan, messages["fwarn_done"]) return @@ -8553,7 +8588,12 @@ def fwarn(cli, nick, chan, rest): notes = notes.strip() # convert expires into a proper datetime - if expires is not None: + if expires is None: + expires = var.DEFAULT_EXPIRY + + if expires.lower() in messages["never_aliases"]: + expires = None + else: suffix = expires[-1] try: amount = int(expires[:-1]) From 1bf468224ec7475e17cbb3e289f572bae3bb8363 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Tue, 14 Jun 2016 16:47:13 -0500 Subject: [PATCH 08/10] Update syntax descriptions --- messages/en.json | 2 +- src/wolfgame.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/messages/en.json b/messages/en.json index 739b9bf..c684519 100644 --- a/messages/en.json +++ b/messages/en.json @@ -802,7 +802,7 @@ "fwarn_list_syntax": "Usage: fwarn list [-all] [nick[!user@host]|=account] [page]", "fwarn_view_syntax": "Usage: fwarn view ", "fwarn_del_syntax": "Usage: fwarn del ", - "fwarn_set_syntax": "Usage: fwarn set [| notes]", + "fwarn_set_syntax": "Usage: fwarn set [~expiry] [reason] [| notes]", "fwarn_help_syntax": "Usage: fwarn help ", "warn_list_syntax": "Usage: warn list [-all] [page]", "warn_view_syntax": "Usage: warn view ", diff --git a/src/wolfgame.py b/src/wolfgame.py index fcaeb8c..c9ff64b 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -8250,7 +8250,7 @@ def fwarn(cli, nick, chan, rest): # use =account. If not specified, shows all warnings on the bot. # !fwarn view - views details on warning id # !fwarn del - deletes warning id - # !fwarn set [| notes] + # !fwarn set [~expiry] [reason] [| notes] # !fwarn add [@] [~expiry] [sanctions] <:reason> [| notes] # e.g. !fwarn add lykos @1 ~30d deny=goat,gstats stasis=5 :Spamming | I secretly just hate him # nick => nick to warn. Can also be a hostmask in nick!user@host form. If nick is not online, From 16a0a28e360087b90b0640c724fe2b0039f81697 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Tue, 14 Jun 2016 17:05:05 -0500 Subject: [PATCH 09/10] Re-add botconfig.ADMINS and ADMINS_ACCOUNTS Still considered legacy/backwards-compat code, so giving out +F flags should be preferred, but now wildcard admins are possible again. --- src/decorators.py | 2 +- src/settings.py | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/decorators.py b/src/decorators.py index ac3af7d..4d253d0 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -189,7 +189,7 @@ class cmd: return flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc] - is_full_admin = "F" in flags + is_full_admin = var.is_admin(nick, ident, host) if self.flag and (is_full_admin or is_owner): adminlog(chan, rawnick, self.name, rest) return self.func(*largs) diff --git a/src/settings.py b/src/settings.py index 61a4c08..520c5da 100644 --- a/src/settings.py +++ b/src/settings.py @@ -377,7 +377,27 @@ def is_admin(nick, ident=None, host=None, acc=None): acc = USERS[nick]["account"] hostmask = nick + "!" + ident + "@" + host flags = FLAGS[hostmask] + FLAGS_ACCS[acc] - return "F" in flags + + if not "F" in flags: + try: + hosts = set(botconfig.ADMINS) + accounts = set(botconfig.ADMINS_ACCOUNTS) + + if not DISABLE_ACCOUNTS and acc and acc != "*": + for pattern in accounts: + if fnmatch.fnmatch(acc.lower(), pattern.lower()): + return True + + if host: + for hostmask in hosts: + if match_hostmask(hostmask, nick, ident, host): + return True + except AttributeError: + pass + + return is_owner(nick, ident, host, acc) + + return True def irc_lower(nick): mapping = { From 79c70fdeb3e3bb89c0a280dc1bcfec7faf0b386c Mon Sep 17 00:00:00 2001 From: skizzerz Date: Mon, 20 Jun 2016 13:44:09 -0500 Subject: [PATCH 10/10] Adjust messages --- messages/en.json | 6 +++--- src/settings.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/messages/en.json b/messages/en.json index c684519..0cb0f13 100644 --- a/messages/en.json +++ b/messages/en.json @@ -817,9 +817,9 @@ "fwarn_added": "Added warning {0}.", "fwarn_done": "Done.", "fwarn_sanction_invalid": "Invalid sanction, can be either deny or stasis.", - "fwarn_stasis_invalid": "Invalid stasis amount, specify sanction as stasis=number.", - "fwarn_deny_invalid": "Invalid denied commands, specify sanction as deny=command,command,command without spaces.", - "fwarn_deny_invalid_command": "Invalid command \"{0}\", specify sanction as deny=command,command,command without spaces.", + "fwarn_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. Warnings prefixed with \u0002!\u0002 are unacknowledged.", "warn_list_header": "You have {0} active warning points. You must acknowledge all warnings prefixed with \u0002!\u0002 by using \"warn ack \" before you can join games.", "fwarn_list": "{0}{1}[#{2} {3}] to {4} by {5} - {6} ({7} points, {8}){9}", diff --git a/src/settings.py b/src/settings.py index 520c5da..c1820a4 100644 --- a/src/settings.py +++ b/src/settings.py @@ -434,8 +434,10 @@ def plural(role, count=2): def singular(plural): # converse of plural above (kinda) - # this is used to map plural role names back to singular, + # this is used to map plural team names back to singular, # so we don't need to worry about stuff like possessives + # Note that this is currently only ever called on team names, + # and will require adjustment if one wishes to use it on roles. conv = {"wolves": "wolf", "succubi": "succubus"} if plural in conv: