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}"
|
SERVER_PASS = "{account}:{password}"
|
||||||
|
|
||||||
OWNERS = ("unaffiliated/wolfbot_admin1",) # The comma is required at the end if there is only one owner.
|
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",)
|
OWNERS_ACCOUNTS = ("1owner_acc",)
|
||||||
ADMINS_ACCOUNTS = ("1admin_acc", "2admin_acc")
|
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_NORMAL_MODE_COMMANDS = [] # Debug mode commands to be allowed in normal mode
|
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
|
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.",
|
"account_not_in_stasis": "\u0002{0}\u0002 (Account: {1}) is not in stasis.",
|
||||||
"currently_stasised": "Currently stasised: {0}",
|
"currently_stasised": "Currently stasised: {0}",
|
||||||
"noone_stasised": "Nobody is currently stasised.",
|
"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?",
|
"no_command_specified": "Error: No command specified. Did you mean \u0002-cmds\u0002?",
|
||||||
"invalid_option": "Invalid option: {0}",
|
"invalid_option": "Invalid option: {0}",
|
||||||
"command_does_not_exist": "That command does not exist.",
|
"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_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.",
|
"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\".",
|
"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:"
|
"_": " vim: set sw=4 expandtab:"
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import time
|
|||||||
|
|
||||||
import botconfig
|
import botconfig
|
||||||
import src.settings as var
|
import src.settings as var
|
||||||
|
from src import db
|
||||||
|
|
||||||
# Segue to logger, since src.gamemodes requires it
|
# 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
|
# 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.DEBUG_MODE = debug_mode if not normal else False
|
||||||
botconfig.VERBOSE_MODE = verbose if not normal else False
|
botconfig.VERBOSE_MODE = verbose if not normal else False
|
||||||
|
|
||||||
# Initialize Database
|
|
||||||
|
|
||||||
var.init_db()
|
|
||||||
|
|
||||||
# Logger
|
# Logger
|
||||||
|
|
||||||
# replace characters that can't be encoded with '?'
|
# 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 botconfig
|
||||||
import src.settings as var
|
import src.settings as var
|
||||||
from src.utilities import *
|
from src.utilities import *
|
||||||
from src import logger
|
from src import logger, db
|
||||||
from src.messages import messages
|
from src.messages import messages
|
||||||
|
|
||||||
adminlog = logger("audit.log")
|
adminlog = logger("audit.log")
|
||||||
@ -66,12 +66,12 @@ class handle_error:
|
|||||||
cli.msg(botconfig.DEV_CHANNEL, " ".join((msg, url)))
|
cli.msg(botconfig.DEV_CHANNEL, " ".join((msg, url)))
|
||||||
|
|
||||||
class cmd:
|
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=()):
|
chan=True, pm=False, playing=False, silenced=False, phases=(), roles=()):
|
||||||
|
|
||||||
self.cmds = cmds
|
self.cmds = cmds
|
||||||
self.raw_nick = raw_nick
|
self.raw_nick = raw_nick
|
||||||
self.admin_only = admin_only
|
self.flag = flag
|
||||||
self.owner_only = owner_only
|
self.owner_only = owner_only
|
||||||
self.chan = chan
|
self.chan = chan
|
||||||
self.pm = pm
|
self.pm = pm
|
||||||
@ -88,7 +88,7 @@ class cmd:
|
|||||||
for name in cmds:
|
for name in cmds:
|
||||||
for func in COMMANDS[name]:
|
for func in COMMANDS[name]:
|
||||||
if (func.owner_only != owner_only or
|
if (func.owner_only != owner_only or
|
||||||
func.admin_only != admin_only):
|
func.flag != flag):
|
||||||
raise ValueError("unmatching protection levels for " + func.name)
|
raise ValueError("unmatching protection levels for " + func.name)
|
||||||
|
|
||||||
COMMANDS[name].append(self)
|
COMMANDS[name].append(self)
|
||||||
@ -125,7 +125,7 @@ class cmd:
|
|||||||
if not self.chan and chan != nick:
|
if not self.chan and chan != nick:
|
||||||
return # channel command, not allowed
|
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:
|
if "" in self.cmds:
|
||||||
return # don't have empty commands triggering in other channels
|
return # don't have empty commands triggering in other channels
|
||||||
for command in self.cmds:
|
for command in self.cmds:
|
||||||
@ -138,6 +138,7 @@ class cmd:
|
|||||||
acc = var.USERS[nick]["account"]
|
acc = var.USERS[nick]["account"]
|
||||||
else:
|
else:
|
||||||
acc = None
|
acc = None
|
||||||
|
hostmask = nick + "!" + ident + "@" + host
|
||||||
|
|
||||||
if "" in self.cmds:
|
if "" in self.cmds:
|
||||||
return self.func(*largs)
|
return self.func(*largs)
|
||||||
@ -175,8 +176,9 @@ class cmd:
|
|||||||
forced_owner_only = True
|
forced_owner_only = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
is_owner = var.is_owner(nick, ident, host)
|
||||||
if self.owner_only or forced_owner_only:
|
if self.owner_only or forced_owner_only:
|
||||||
if var.is_owner(nick, ident, host):
|
if is_owner:
|
||||||
adminlog(chan, rawnick, self.name, rest)
|
adminlog(chan, rawnick, self.name, rest)
|
||||||
return self.func(*largs)
|
return self.func(*largs)
|
||||||
|
|
||||||
@ -186,49 +188,26 @@ class cmd:
|
|||||||
cli.notice(nick, messages["not_owner"])
|
cli.notice(nick, messages["not_owner"])
|
||||||
return
|
return
|
||||||
|
|
||||||
if var.is_admin(nick, ident, host):
|
flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc]
|
||||||
if self.admin_only:
|
is_full_admin = var.is_admin(nick, ident, host)
|
||||||
adminlog(chan, rawnick, self.name, rest)
|
if self.flag and (is_full_admin or is_owner):
|
||||||
|
adminlog(chan, rawnick, self.name, rest)
|
||||||
return self.func(*largs)
|
return self.func(*largs)
|
||||||
|
|
||||||
if not var.DISABLE_ACCOUNTS and acc:
|
denied_cmds = var.DENY[hostmask] | var.DENY_ACCS[acc]
|
||||||
if acc in var.DENY_ACCOUNTS:
|
for command in self.cmds:
|
||||||
for command in self.cmds:
|
if command in denied_cmds:
|
||||||
if command in var.DENY_ACCOUNTS[acc]:
|
if chan == nick:
|
||||||
if chan == nick:
|
pm(cli, nick, messages["invalid_permissions"])
|
||||||
pm(cli, nick, messages["invalid_permissions"])
|
else:
|
||||||
else:
|
cli.notice(nick, messages["invalid_permissions"])
|
||||||
cli.notice(nick, messages["invalid_permissions"])
|
return
|
||||||
return
|
|
||||||
|
|
||||||
if acc in var.ALLOW_ACCOUNTS:
|
if self.flag:
|
||||||
for command in self.cmds:
|
if self.flag in flags:
|
||||||
if command in var.ALLOW_ACCOUNTS[acc]:
|
adminlog(chan, rawnick, self.name, rest)
|
||||||
if self.admin_only:
|
return self.func(*largs)
|
||||||
adminlog(chan, rawnick, self.name, rest)
|
elif chan == nick:
|
||||||
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:
|
|
||||||
pm(cli, nick, messages["not_an_admin"])
|
pm(cli, nick, messages["not_an_admin"])
|
||||||
else:
|
else:
|
||||||
cli.notice(nick, messages["not_an_admin"])
|
cli.notice(nick, messages["not_an_admin"])
|
||||||
@ -264,3 +243,5 @@ class hook:
|
|||||||
HOOKS[each].remove(inner)
|
HOOKS[each].remove(inner)
|
||||||
if not HOOKS[each]:
|
if not HOOKS[each]:
|
||||||
del 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;
|
505
src/settings.py
505
src/settings.py
@ -1,6 +1,6 @@
|
|||||||
import fnmatch
|
import fnmatch
|
||||||
import sqlite3
|
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
from collections import defaultdict, OrderedDict
|
from collections import defaultdict, OrderedDict
|
||||||
|
|
||||||
import botconfig
|
import botconfig
|
||||||
@ -52,10 +52,6 @@ START_QUIT_DELAY = 10
|
|||||||
MAX_PRIVMSG_TARGETS = 4
|
MAX_PRIVMSG_TARGETS = 4
|
||||||
# how many mode values can be specified at once; used only as fallback
|
# how many mode values can be specified at once; used only as fallback
|
||||||
MODELIMIT = 3
|
MODELIMIT = 3
|
||||||
LEAVE_STASIS_PENALTY = 1
|
|
||||||
IDLE_STASIS_PENALTY = 1
|
|
||||||
PART_STASIS_PENALTY = 1
|
|
||||||
ACC_STASIS_PENALTY = 1
|
|
||||||
QUIET_DEAD_PLAYERS = False
|
QUIET_DEAD_PLAYERS = False
|
||||||
DEVOICE_DURING_NIGHT = False
|
DEVOICE_DURING_NIGHT = False
|
||||||
ALWAYS_PM_ROLE = False
|
ALWAYS_PM_ROLE = False
|
||||||
@ -64,6 +60,34 @@ QUIET_PREFIX = "" # "" or "~q:"
|
|||||||
# The bot will automatically toggle those modes of people joining
|
# The bot will automatically toggle those modes of people joining
|
||||||
AUTO_TOGGLE_MODES = ""
|
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
|
# 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
|
# 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_MODES = {}
|
||||||
GAME_PHASES = ("night", "day") # all phases that constitute "in game", game modes can extend this with custom phases
|
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
|
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
|
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 = "ChanServ"
|
||||||
CHANSERV_OP_COMMAND = "OP {channel}"
|
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")
|
# 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"
|
DEFAULT_ROLE = "villager"
|
||||||
ROLE_INDEX = ( 4 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 15 , 16 , 18 , 20 , 21 , 23 , 24 )
|
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
|
GIF_CHANCE = 1/50
|
||||||
FORTUNE_CHANCE = 1/25
|
FORTUNE_CHANCE = 1/25
|
||||||
|
|
||||||
|
ALL_FLAGS = frozenset("AaDdFjms")
|
||||||
|
|
||||||
RULES = (botconfig.CHANNEL + " channel rules: http://wolf.xnrand.com/rules")
|
RULES = (botconfig.CHANNEL + " channel rules: http://wolf.xnrand.com/rules")
|
||||||
DENY = {}
|
|
||||||
ALLOW = {}
|
|
||||||
|
|
||||||
DENY_ACCOUNTS = {}
|
GRAVEYARD_LOCK = threading.RLock()
|
||||||
ALLOW_ACCOUNTS = {}
|
WARNING_LOCK = threading.RLock()
|
||||||
|
WAIT_TB_LOCK = threading.RLock()
|
||||||
|
|
||||||
# pingif-related mappings
|
#TODO: move all of these to util.py or other files, as they are certainly NOT settings!
|
||||||
|
|
||||||
PING_IF_PREFS = {}
|
|
||||||
PING_IF_PREFS_ACCS = {}
|
|
||||||
|
|
||||||
PING_IF_NUMS = {}
|
|
||||||
PING_IF_NUMS_ACCS = {}
|
|
||||||
|
|
||||||
DEADCHAT_PREFS = set()
|
|
||||||
DEADCHAT_PREFS_ACCS = set()
|
|
||||||
|
|
||||||
is_role = lambda plyr, rol: rol in ROLES and plyr in ROLES[rol]
|
is_role = lambda plyr, rol: rol in ROLES and plyr in ROLES[rol]
|
||||||
|
|
||||||
@ -336,43 +344,60 @@ def match_hostmask(hostmask, nick, ident, host):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def is_owner(nick, ident=None, host=None, acc=None):
|
||||||
def check_priv(priv):
|
|
||||||
assert priv in ("owner", "admin")
|
|
||||||
|
|
||||||
# Owners can do everything
|
|
||||||
hosts = set(botconfig.OWNERS)
|
hosts = set(botconfig.OWNERS)
|
||||||
accounts = set(botconfig.OWNERS_ACCOUNTS)
|
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":
|
if not DISABLE_ACCOUNTS and acc and acc != "*":
|
||||||
hosts.update(botconfig.ADMINS)
|
for pattern in accounts:
|
||||||
accounts.update(botconfig.ADMINS_ACCOUNTS)
|
if fnmatch.fnmatch(acc.lower(), pattern.lower()):
|
||||||
|
return True
|
||||||
|
|
||||||
def do_check(nick, ident=None, host=None, acc=None):
|
if host:
|
||||||
if nick in USERS.keys():
|
for hostmask in hosts:
|
||||||
if not ident:
|
if match_hostmask(hostmask, nick, ident, host):
|
||||||
ident = USERS[nick]["ident"]
|
return True
|
||||||
if not host:
|
|
||||||
host = USERS[nick]["host"]
|
|
||||||
if not acc:
|
|
||||||
acc = USERS[nick]["account"]
|
|
||||||
|
|
||||||
if not DISABLE_ACCOUNTS and acc and acc != "*":
|
return False
|
||||||
for pattern in accounts:
|
|
||||||
if fnmatch.fnmatch(acc.lower(), pattern.lower()):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if host:
|
def is_admin(nick, ident=None, host=None, acc=None):
|
||||||
for hostmask in hosts:
|
if nick in USERS:
|
||||||
if match_hostmask(hostmask, nick, ident, host):
|
if not ident:
|
||||||
return True
|
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]
|
||||||
|
|
||||||
return False
|
if not "F" in flags:
|
||||||
|
try:
|
||||||
|
hosts = set(botconfig.ADMINS)
|
||||||
|
accounts = set(botconfig.ADMINS_ACCOUNTS)
|
||||||
|
|
||||||
return do_check
|
if not DISABLE_ACCOUNTS and acc and acc != "*":
|
||||||
|
for pattern in accounts:
|
||||||
|
if fnmatch.fnmatch(acc.lower(), pattern.lower()):
|
||||||
|
return True
|
||||||
|
|
||||||
is_admin = check_priv("admin")
|
if host:
|
||||||
is_owner = check_priv("owner")
|
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):
|
def irc_lower(nick):
|
||||||
mapping = {
|
mapping = {
|
||||||
@ -393,7 +418,6 @@ def irc_equals(nick1, nick2):
|
|||||||
return irc_lower(nick1) == irc_lower(nick2)
|
return irc_lower(nick1) == irc_lower(nick2)
|
||||||
|
|
||||||
def plural(role, count=2):
|
def plural(role, count=2):
|
||||||
# TODO: use the "inflect" pip package, pass part-of-speech as a kwarg
|
|
||||||
if count == 1:
|
if count == 1:
|
||||||
return role
|
return role
|
||||||
bits = role.split()
|
bits = role.split()
|
||||||
@ -408,6 +432,19 @@ def plural(role, count=2):
|
|||||||
"child": "children"}.get(bits[-1], bits[-1] + "s")
|
"child": "children"}.get(bits[-1], bits[-1] + "s")
|
||||||
return " ".join(bits)
|
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):
|
def list_players(roles = None):
|
||||||
if roles is None:
|
if roles is None:
|
||||||
roles = ROLES.keys()
|
roles = ROLES.keys()
|
||||||
@ -501,362 +538,4 @@ def break_long_message(phrases, joinstr = " "):
|
|||||||
|
|
||||||
class InvalidModeException(Exception): pass
|
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:
|
# vim: set sw=4 expandtab:
|
||||||
|
1478
src/wolfgame.py
1478
src/wolfgame.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user