Merge pull request #225 from lykoss/newdb
- new schema, including stats tracking - 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 - fflags/ftemplate for permissions revamp
This commit is contained in:
commit
c08cd3efbc
@ -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
|
||||
|
@ -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.",
|
||||
@ -796,6 +797,69 @@
|
||||
"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|set|help. See fwarn help <subcommand> for more details.",
|
||||
"warn_usage": "Usage: warn list|view|ack|help. See warn help <subcommand> for more details.",
|
||||
"fwarn_list_syntax": "Usage: fwarn list [-all] [nick[!user@host]|=account] [page]",
|
||||
"fwarn_view_syntax": "Usage: fwarn view <id>",
|
||||
"fwarn_del_syntax": "Usage: fwarn del <id>",
|
||||
"fwarn_set_syntax": "Usage: fwarn set <id> [~expiry] [reason] [| notes]",
|
||||
"fwarn_help_syntax": "Usage: fwarn help <subcommand>",
|
||||
"warn_list_syntax": "Usage: warn list [-all] [page]",
|
||||
"warn_view_syntax": "Usage: warn view <id>",
|
||||
"warn_ack_syntax": "Usage: warn ack <id>",
|
||||
"warn_help_syntax": "Uwage: warn help <subcommand>",
|
||||
"fwarn_add_syntax": "Usage: fwarn add <nick[!user@host]|=account> [@]<points> [~expiry] [sanctions] <:reason> [| notes]",
|
||||
"fwarn_page_invalid": "Invalid page, must be a number 1 or greater.",
|
||||
"fwarn_points_invalid": "Invalid points, must be a number above 0.",
|
||||
"fwarn_expiry_invalid": "Invalid expiration amount, must be a number above 0 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}.",
|
||||
"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. Warnings prefixed with \u0002!\u0002 are unacknowledged.",
|
||||
"warn_list_header": "You have {0} active warning points. You must acknowledge all warnings prefixed with \u0002!\u0002 by using \"warn ack <id>\" before you can join games.",
|
||||
"fwarn_list": "{0}{1}[#{2} {3}] to {4} by {5} - {6} ({7} points, {8}){9}",
|
||||
"warn_list": "{0}{1}[#{2} {3}] {4} ({5} points, {6}){7}",
|
||||
"fwarn_deleted": "deleted",
|
||||
"fwarn_expired": "expired",
|
||||
"fwarn_list_expired": "expired on {0}",
|
||||
"fwarn_never_expires": "never expires",
|
||||
"fwarn_list_footer": "More results are available, use fwarn list {0} to view them.",
|
||||
"warn_list_footer": "More results are available, use warn list {0} to view them.",
|
||||
"fwarn_list_empty": "No results.",
|
||||
"fwarn_invalid_warning": "The specified warning id does not exist or you do not have permission to view it.",
|
||||
"fwarn_view_header": "Warning #{0}, given to {1} on {2} by {3}. {4} points. {5}.",
|
||||
"warn_view_header": "Warning #{0}, given on {1}. {2} points. {3}.",
|
||||
"fwarn_view_active": "Currently active, {0}",
|
||||
"fwarn_view_expires": "expires on {0}",
|
||||
"fwarn_view_expired": "Expired on {0}",
|
||||
"fwarn_view_deleted": "Deleted on {0} by {1}",
|
||||
"fwarn_view_ack": "Warning has not yet been acknowledged.",
|
||||
"warn_view_ack": "You have not yet acknowledge this warning. You must acknowledge this warning by using \"warn ack {0}\" before you can join games.",
|
||||
"fwarn_view_sanctions": "Sanctions:",
|
||||
"fwarn_view_stasis_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.",
|
||||
"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}.",
|
||||
"never_aliases": ["never", "infinite", "infinity", "permanent", "p"],
|
||||
|
||||
"_": " vim: set sw=4 expandtab:"
|
||||
}
|
||||
|
@ -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
|
||||
@ -83,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
|
||||
|
||||
var.init_db()
|
||||
|
||||
# Logger
|
||||
|
||||
# replace characters that can't be encoded with '?'
|
||||
|
807
src/db.py
Normal file
807
src/db.py
Normal file
@ -0,0 +1,807 @@
|
||||
import botconfig
|
||||
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
|
||||
|
||||
def init_vars():
|
||||
with var.GRAVEYARD_LOCK:
|
||||
c = conn.cursor()
|
||||
c.execute("""SELECT
|
||||
pl.account,
|
||||
pl.hostmask,
|
||||
pe.notice,
|
||||
pe.simple,
|
||||
pe.deadchat,
|
||||
pe.pingif,
|
||||
pe.stasis_amount,
|
||||
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""")
|
||||
|
||||
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)
|
||||
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)
|
||||
if flags:
|
||||
var.FLAGS_ACCS[acc] = flags
|
||||
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)
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
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 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_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 has_unacknowledged_warnings(acc, hostmask):
|
||||
peid, plid = _get_ids(acc, hostmask)
|
||||
if peid is None:
|
||||
return False
|
||||
c = conn.cursor()
|
||||
c.execute("""SELECT COALESCE(MIN(acknowledged), 1)
|
||||
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
|
||||
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,
|
||||
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
|
||||
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
|
||||
"""
|
||||
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, expired=False, deleted=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 0 ELSE 1 END AS expired,
|
||||
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
|
||||
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
|
||||
WHERE
|
||||
warning.target = ?
|
||||
"""
|
||||
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"
|
||||
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 get_warning(warn_id, acc=None, hm=None):
|
||||
peid, plid = _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)
|
||||
ack = 0 if need_ack else 1
|
||||
with conn:
|
||||
c = conn.cursor()
|
||||
c.execute("""INSERT INTO warning
|
||||
(
|
||||
target, sender, amount,
|
||||
issued, expires,
|
||||
reason, notes,
|
||||
acknowledged
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
?, ?, ?,
|
||||
datetime('now'), ?,
|
||||
?, ?,
|
||||
?
|
||||
)""", (teid, seid, amount, expires, reason, notes, ack))
|
||||
return c.lastrowid
|
||||
|
||||
def add_warning_sanction(warning, sanction, data):
|
||||
with conn:
|
||||
c = conn.cursor()
|
||||
c.execute("""INSERT INTO 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:
|
||||
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, expires, reason, notes):
|
||||
with conn:
|
||||
c = conn.cursor()
|
||||
c.execute("""UPDATE warning
|
||||
SET reason = ?, notes = ?, expires = ?
|
||||
WHERE id = ?""", (reason, notes, expires, 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
|
||||
# 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)
|
||||
|
||||
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:
|
171
src/db.sql
Normal file
171
src/db.sql
Normal file
@ -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
|
||||
);
|
@ -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")
|
||||
@ -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)
|
||||
@ -175,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)
|
||||
|
||||
@ -186,49 +188,26 @@ class cmd:
|
||||
cli.notice(nick, messages["not_owner"])
|
||||
return
|
||||
|
||||
if var.is_admin(nick, ident, host):
|
||||
if self.admin_only:
|
||||
flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc]
|
||||
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)
|
||||
|
||||
if not var.DISABLE_ACCOUNTS and acc:
|
||||
if acc in var.DENY_ACCOUNTS:
|
||||
denied_cmds = var.DENY[hostmask] | var.DENY_ACCS[acc]
|
||||
for command in self.cmds:
|
||||
if command in var.DENY_ACCOUNTS[acc]:
|
||||
if command in denied_cmds:
|
||||
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:
|
||||
if self.flag:
|
||||
if self.flag in flags:
|
||||
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 chan == nick:
|
||||
elif chan == nick:
|
||||
pm(cli, nick, messages["not_an_admin"])
|
||||
else:
|
||||
cli.notice(nick, messages["not_an_admin"])
|
||||
@ -264,3 +243,5 @@ class hook:
|
||||
HOOKS[each].remove(inner)
|
||||
if not HOOKS[each]:
|
||||
del HOOKS[each]
|
||||
|
||||
# vim: set sw=4 expandtab:
|
||||
|
214
src/migrate.sql
Normal file
214
src/migrate.sql
Normal file
@ -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;
|
479
src/settings.py
479
src/settings.py
@ -1,6 +1,6 @@
|
||||
import fnmatch
|
||||
import sqlite3
|
||||
import re
|
||||
import threading
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
import botconfig
|
||||
@ -52,10 +52,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 +60,34 @@ 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
|
||||
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}),
|
||||
(11, 14, {"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
|
||||
|
||||
@ -174,10 +198,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
|
||||
@ -191,9 +211,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 )
|
||||
@ -304,24 +321,15 @@ 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")
|
||||
DENY = {}
|
||||
ALLOW = {}
|
||||
|
||||
DENY_ACCOUNTS = {}
|
||||
ALLOW_ACCOUNTS = {}
|
||||
GRAVEYARD_LOCK = threading.RLock()
|
||||
WARNING_LOCK = threading.RLock()
|
||||
WAIT_TB_LOCK = threading.RLock()
|
||||
|
||||
# pingif-related mappings
|
||||
|
||||
PING_IF_PREFS = {}
|
||||
PING_IF_PREFS_ACCS = {}
|
||||
|
||||
PING_IF_NUMS = {}
|
||||
PING_IF_NUMS_ACCS = {}
|
||||
|
||||
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]
|
||||
|
||||
@ -336,20 +344,10 @@ 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 priv == "admin":
|
||||
hosts.update(botconfig.ADMINS)
|
||||
accounts.update(botconfig.ADMINS_ACCOUNTS)
|
||||
|
||||
def do_check(nick, ident=None, host=None, acc=None):
|
||||
if nick in USERS.keys():
|
||||
if nick in USERS:
|
||||
if not ident:
|
||||
ident = USERS[nick]["ident"]
|
||||
if not host:
|
||||
@ -369,10 +367,37 @@ def check_priv(priv):
|
||||
|
||||
return False
|
||||
|
||||
return do_check
|
||||
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 = FLAGS[hostmask] + FLAGS_ACCS[acc]
|
||||
|
||||
is_admin = check_priv("admin")
|
||||
is_owner = check_priv("owner")
|
||||
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 = {
|
||||
@ -393,7 +418,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 +432,19 @@ 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 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:
|
||||
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 +538,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:
|
||||
|
1526
src/wolfgame.py
1526
src/wolfgame.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user