Additional text at the end (local variables) has a red border for each line with py3tb, but not with pytb.
494 lines
17 KiB
Python
494 lines
17 KiB
Python
import fnmatch
|
|
import itertools
|
|
import json
|
|
import random
|
|
import re
|
|
import string
|
|
import traceback
|
|
import urllib
|
|
|
|
import botconfig
|
|
import src.settings as var
|
|
from src import proxy, debuglog
|
|
from src.events import Event
|
|
from src.messages import messages
|
|
|
|
__all__ = ["pm", "is_fake_nick", "mass_mode", "mass_privmsg", "reply",
|
|
"is_user_simple", "is_user_notice", "in_wolflist",
|
|
"relay_wolfchat_command", "chk_nightdone", "chk_decision",
|
|
"chk_win", "irc_lower", "irc_equals", "is_role", "match_hostmask",
|
|
"is_owner", "is_admin", "plural", "singular", "list_players",
|
|
"list_players_and_roles", "list_participants", "get_role", "get_roles",
|
|
"get_reveal_role", "get_templates", "role_order", "break_long_message",
|
|
"complete_match", "get_victim", "get_nick", "pastebin_tb",
|
|
"InvalidModeException"]
|
|
# message either privmsg or notice, depending on user settings
|
|
def pm(cli, target, message):
|
|
if is_fake_nick(target) and botconfig.DEBUG_MODE:
|
|
debuglog("Would message fake nick {0}: {1!r}".format(target, message))
|
|
return
|
|
|
|
if is_user_notice(target):
|
|
cli.notice(target, message)
|
|
return
|
|
|
|
cli.msg(target, message)
|
|
|
|
is_fake_nick = re.compile(r"^[0-9]+$").search
|
|
|
|
def mass_mode(cli, md_param, md_plain):
|
|
""" Example: mass_mode(cli, [('+v', 'asdf'), ('-v','wobosd')], ['-m']) """
|
|
lmd = len(md_param) # store how many mode changes to do
|
|
if md_param:
|
|
for start_i in range(0, lmd, var.MODELIMIT): # 4 mode-changes at a time
|
|
if start_i + var.MODELIMIT > lmd: # If this is a remainder (mode-changes < 4)
|
|
z = list(zip(*md_param[start_i:])) # zip this remainder
|
|
ei = lmd % var.MODELIMIT # len(z)
|
|
else:
|
|
z = list(zip(*md_param[start_i:start_i+var.MODELIMIT])) # zip four
|
|
ei = var.MODELIMIT # len(z)
|
|
# Now z equal something like [('+v', '-v'), ('asdf', 'wobosd')]
|
|
arg1 = "".join(md_plain) + "".join(z[0])
|
|
arg2 = " ".join(z[1]) # + " " + " ".join([x+"!*@*" for x in z[1]])
|
|
cli.mode(botconfig.CHANNEL, arg1, arg2)
|
|
elif md_plain:
|
|
cli.mode(botconfig.CHANNEL, "".join(md_plain))
|
|
|
|
def mass_privmsg(cli, targets, msg, notice=False, privmsg=False):
|
|
if not targets:
|
|
return
|
|
if not notice and not privmsg:
|
|
msg_targs = []
|
|
not_targs = []
|
|
for target in targets:
|
|
if is_fake_nick(target):
|
|
debuglog("Would message fake nick {0}: {1!r}".format(target, msg))
|
|
elif is_user_notice(target):
|
|
not_targs.append(target)
|
|
else:
|
|
msg_targs.append(target)
|
|
while msg_targs:
|
|
if len(msg_targs) <= var.MAX_PRIVMSG_TARGETS:
|
|
bgs = ",".join(msg_targs)
|
|
msg_targs = None
|
|
else:
|
|
bgs = ",".join(msg_targs[:var.MAX_PRIVMSG_TARGETS])
|
|
msg_targs = msg_targs[var.MAX_PRIVMSG_TARGETS:]
|
|
cli.msg(bgs, msg)
|
|
while not_targs:
|
|
if len(not_targs) <= var.MAX_PRIVMSG_TARGETS:
|
|
bgs = ",".join(not_targs)
|
|
not_targs = None
|
|
else:
|
|
bgs = ",".join(not_targs[:var.MAX_PRIVMSG_TARGETS])
|
|
not_targs = not_targs[var.MAX_PRIVMSG_TARGETS:]
|
|
cli.notice(bgs, msg)
|
|
else:
|
|
while targets:
|
|
if len(targets) <= var.MAX_PRIVMSG_TARGETS:
|
|
bgs = ",".join(targets)
|
|
targets = None
|
|
else:
|
|
bgs = ",".join(targets[:var.MAX_PRIVMSG_TARGETS])
|
|
target = targets[var.MAX_PRIVMSG_TARGETS:]
|
|
if notice:
|
|
cli.notice(bgs, msg)
|
|
else:
|
|
cli.msg(bgs, msg)
|
|
|
|
# Decide how to reply to a user, depending on the channel / query it was called in, and whether a game is running and they are playing
|
|
def reply(cli, nick, chan, msg, private=False, prefix_nick=False):
|
|
if chan == nick:
|
|
pm(cli, nick, msg)
|
|
elif private or (chan == botconfig.CHANNEL and
|
|
((nick not in list_players() and var.PHASE in var.GAME_PHASES) or
|
|
(var.DEVOICE_DURING_NIGHT and var.PHASE == "night"))):
|
|
cli.notice(nick, msg)
|
|
else:
|
|
if prefix_nick:
|
|
cli.msg(chan, "{0}: {1}".format(nick, msg))
|
|
else:
|
|
cli.msg(chan, msg)
|
|
|
|
def is_user_simple(nick):
|
|
if nick in var.USERS:
|
|
ident = irc_lower(var.USERS[nick]["ident"])
|
|
host = var.USERS[nick]["host"].lower()
|
|
acc = irc_lower(var.USERS[nick]["account"])
|
|
else:
|
|
return False
|
|
if acc and acc != "*" and not var.DISABLE_ACCOUNTS:
|
|
if acc in var.SIMPLE_NOTIFY_ACCS:
|
|
return True
|
|
return False
|
|
elif not var.ACCOUNTS_ONLY:
|
|
for hostmask in var.SIMPLE_NOTIFY:
|
|
if match_hostmask(hostmask, nick, ident, host):
|
|
return True
|
|
return False
|
|
|
|
def is_user_notice(nick):
|
|
if nick in var.USERS and var.USERS[nick]["account"] and var.USERS[nick]["account"] != "*" and not var.DISABLE_ACCOUNTS:
|
|
if irc_lower(var.USERS[nick]["account"]) in var.PREFER_NOTICE_ACCS:
|
|
return True
|
|
if nick in var.USERS and not var.ACCOUNTS_ONLY:
|
|
ident = irc_lower(var.USERS[nick]["ident"])
|
|
host = var.USERS[nick]["host"].lower()
|
|
for hostmask in var.PREFER_NOTICE:
|
|
if match_hostmask(hostmask, nick, ident, host):
|
|
return True
|
|
return False
|
|
|
|
def in_wolflist(nick, who):
|
|
myrole = get_role(nick)
|
|
role = get_role(who)
|
|
wolves = var.WOLFCHAT_ROLES
|
|
if var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES:
|
|
if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF:
|
|
wolves = var.WOLF_ROLES
|
|
else:
|
|
wolves = var.WOLF_ROLES | {"traitor"}
|
|
return myrole in wolves and role in wolves
|
|
|
|
def relay_wolfchat_command(cli, nick, message, roles, is_wolf_command=False, is_kill_command=False):
|
|
if not is_wolf_command and var.RESTRICT_WOLFCHAT & var.RW_NO_INTERACTION:
|
|
return
|
|
if not is_kill_command and var.RESTRICT_WOLFCHAT & var.RW_ONLY_KILL_CMD:
|
|
if var.PHASE == "night" and var.RESTRICT_WOLFCHAT & var.RW_DISABLE_NIGHT:
|
|
return
|
|
if var.PHASE == "day" and var.RESTRICT_WOLFCHAT & var.RW_DISABLE_DAY:
|
|
return
|
|
if not in_wolflist(nick, nick):
|
|
return
|
|
|
|
wcroles = var.WOLFCHAT_ROLES
|
|
if var.RESTRICT_WOLFCHAT & var.RW_ONLY_SAME_CMD:
|
|
if var.PHASE == "night" and var.RESTRICT_WOLFCHAT & var.RW_DISABLE_NIGHT:
|
|
wcroles = roles
|
|
if var.PHASE == "day" and var.RESTRICT_WOLFCHAT & var.RW_DISABLE_DAY:
|
|
wcroles = roles
|
|
elif var.RESTRICT_WOLFCHAT & var.RW_REM_NON_WOLVES:
|
|
if var.RESTRICT_WOLFCHAT & var.RW_TRAITOR_NON_WOLF:
|
|
wcroles = var.WOLF_ROLES
|
|
else:
|
|
wcroles = var.WOLF_ROLES | {"traitor"}
|
|
|
|
wcwolves = list_players(wcroles)
|
|
wcwolves.remove(nick)
|
|
mass_privmsg(cli, wcwolves, message)
|
|
mass_privmsg(cli, var.SPECTATING_WOLFCHAT, "[wolfchat] " + message)
|
|
|
|
@proxy.stub
|
|
def chk_nightdone(cli):
|
|
pass
|
|
|
|
@proxy.stub
|
|
def chk_decision(cli, force=""):
|
|
pass
|
|
|
|
@proxy.stub
|
|
def chk_win(cli, end_game=True, winner=None):
|
|
pass
|
|
|
|
def irc_lower(nick):
|
|
if nick is None:
|
|
return None
|
|
|
|
mapping = {
|
|
"[": "{",
|
|
"]": "}",
|
|
"\\": "|",
|
|
"^": "~",
|
|
}
|
|
|
|
# var.CASEMAPPING may not be defined yet in some circumstances (like database upgrades)
|
|
# if so, default to rfc1459
|
|
if hasattr(var, "CASEMAPPING"):
|
|
if var.CASEMAPPING == "strict-rfc1459":
|
|
mapping.pop("^")
|
|
elif var.CASEMAPPING == "ascii":
|
|
mapping = {}
|
|
|
|
return nick.lower().translate(str.maketrans(mapping))
|
|
|
|
def irc_equals(nick1, nick2):
|
|
return irc_lower(nick1) == irc_lower(nick2)
|
|
|
|
is_role = lambda plyr, rol: rol in var.ROLES and plyr in var.ROLES[rol]
|
|
|
|
def match_hostmask(hostmask, nick, ident, host):
|
|
# support n!u@h, u@h, or just h by itself
|
|
matches = re.match('(?:(?:(.*?)!)?(.*?)@)?(.*)', hostmask)
|
|
|
|
if ((not matches.group(1) or fnmatch.fnmatch(irc_lower(nick), irc_lower(matches.group(1)))) and
|
|
(not matches.group(2) or fnmatch.fnmatch(irc_lower(ident), irc_lower(matches.group(2)))) and
|
|
fnmatch.fnmatch(host.lower(), matches.group(3).lower())):
|
|
return True
|
|
|
|
return False
|
|
|
|
def is_owner(nick, ident=None, host=None, acc=None):
|
|
hosts = set(botconfig.OWNERS)
|
|
accounts = set(botconfig.OWNERS_ACCOUNTS)
|
|
if nick in var.USERS:
|
|
if not ident:
|
|
ident = var.USERS[nick]["ident"]
|
|
if not host:
|
|
host = var.USERS[nick]["host"]
|
|
if not acc:
|
|
acc = var.USERS[nick]["account"]
|
|
|
|
if not var.DISABLE_ACCOUNTS and acc and acc != "*":
|
|
for pattern in accounts:
|
|
if fnmatch.fnmatch(irc_lower(acc), irc_lower(pattern)):
|
|
return True
|
|
|
|
if host:
|
|
for hostmask in hosts:
|
|
if match_hostmask(hostmask, nick, ident, host):
|
|
return True
|
|
|
|
return False
|
|
|
|
def is_admin(nick, ident=None, host=None, acc=None):
|
|
if nick in var.USERS:
|
|
if not ident:
|
|
ident = var.USERS[nick]["ident"]
|
|
if not host:
|
|
host = var.USERS[nick]["host"]
|
|
if not acc:
|
|
acc = var.USERS[nick]["account"]
|
|
acc = irc_lower(acc)
|
|
hostmask = irc_lower(nick) + "!" + irc_lower(ident) + "@" + host.lower()
|
|
flags = var.FLAGS[hostmask] + var.FLAGS_ACCS[acc]
|
|
|
|
if not "F" in flags:
|
|
try:
|
|
hosts = set(botconfig.ADMINS)
|
|
accounts = set(botconfig.ADMINS_ACCOUNTS)
|
|
|
|
if not var.DISABLE_ACCOUNTS and acc and acc != "*":
|
|
for pattern in accounts:
|
|
if fnmatch.fnmatch(irc_lower(acc), irc_lower(pattern)):
|
|
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 plural(role, count=2):
|
|
if count == 1:
|
|
return role
|
|
bits = role.split()
|
|
if bits[-1][-2:] == "'s":
|
|
bits[-1] = plural(bits[-1][:-2], count)
|
|
bits[-1] += "'" if bits[-1][-1] == "s" else "'s"
|
|
else:
|
|
bits[-1] = {"person": "people",
|
|
"wolf": "wolves",
|
|
"has": "have",
|
|
"succubus": "succubi",
|
|
"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.
|
|
# fool is present since we store fool wins as 'fool' rather than
|
|
# 'fools' as only a single fool wins, however we don't want to
|
|
# chop off the l and have it report 'foo wins'
|
|
conv = {"wolves": "wolf",
|
|
"succubi": "succubus",
|
|
"fool": "fool"}
|
|
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 = var.ROLES.keys()
|
|
pl = set()
|
|
for x in roles:
|
|
if x in var.TEMPLATE_RESTRICTIONS.keys():
|
|
continue
|
|
for p in var.ROLES.get(x, ()):
|
|
pl.add(p)
|
|
return [p for p in var.ALL_PLAYERS if p in pl]
|
|
|
|
def list_players_and_roles():
|
|
plr = {}
|
|
for x in var.ROLES.keys():
|
|
if x in var.TEMPLATE_RESTRICTIONS.keys():
|
|
continue # only get actual roles
|
|
for p in var.ROLES[x]:
|
|
plr[p] = x
|
|
return plr
|
|
|
|
def list_participants():
|
|
"""List all people who are still able to participate in the game in some fashion."""
|
|
pl = list_players()
|
|
evt = Event("list_participants", {"pl": pl})
|
|
evt.dispatch(var)
|
|
return evt.data["pl"][:]
|
|
|
|
def get_role(p):
|
|
for role, pl in var.ROLES.items():
|
|
if role in var.TEMPLATE_RESTRICTIONS.keys():
|
|
continue # only get actual roles
|
|
if p in pl:
|
|
return role
|
|
# not found in player list, see if they're a special participant
|
|
role = None
|
|
if p in list_participants():
|
|
evt = Event("get_participant_role", {"role": None})
|
|
evt.dispatch(var, p)
|
|
role = evt.data["role"]
|
|
if role is None:
|
|
raise ValueError("Nick {0} isn't playing and has no defined participant role".format(p))
|
|
return role
|
|
|
|
def get_roles(*roles):
|
|
all_roles = []
|
|
for role in roles:
|
|
all_roles.append(var.ROLES[role])
|
|
return list(itertools.chain(*all_roles))
|
|
|
|
def get_reveal_role(nick):
|
|
if var.HIDDEN_TRAITOR and get_role(nick) == "traitor":
|
|
role = var.DEFAULT_ROLE
|
|
elif var.HIDDEN_AMNESIAC and nick in var.ORIGINAL_ROLES["amnesiac"]:
|
|
role = "amnesiac"
|
|
elif var.HIDDEN_CLONE and nick in var.ORIGINAL_ROLES["clone"]:
|
|
role = "clone"
|
|
else:
|
|
role = get_role(nick)
|
|
|
|
evt = Event("get_reveal_role", {"role": role})
|
|
evt.dispatch(var, nick)
|
|
role = evt.data["role"]
|
|
|
|
if var.ROLE_REVEAL != "team":
|
|
return role
|
|
|
|
if role in var.WOLFTEAM_ROLES:
|
|
return "wolf"
|
|
elif role in var.TRUE_NEUTRAL_ROLES:
|
|
return "neutral player"
|
|
else:
|
|
return "villager"
|
|
|
|
def get_templates(nick):
|
|
tpl = []
|
|
for x in var.TEMPLATE_RESTRICTIONS.keys():
|
|
try:
|
|
if nick in var.ROLES[x]:
|
|
tpl.append(x)
|
|
except KeyError:
|
|
pass
|
|
|
|
return tpl
|
|
|
|
role_order = lambda: var.ROLE_GUIDE
|
|
|
|
def break_long_message(phrases, joinstr = " "):
|
|
message = []
|
|
count = 0
|
|
for phrase in phrases:
|
|
# IRC max is 512, but freenode splits around 380ish, make 300 to have plenty of wiggle room
|
|
if count + len(joinstr) + len(phrase) > 300:
|
|
message.append("\n" + phrase)
|
|
count = len(phrase)
|
|
else:
|
|
if not message:
|
|
count = len(phrase)
|
|
else:
|
|
count += len(joinstr) + len(phrase)
|
|
message.append(phrase)
|
|
return joinstr.join(message)
|
|
|
|
#completes a partial nickname or string from a list
|
|
def complete_match(string, matches):
|
|
num_matches = 0
|
|
bestmatch = string
|
|
for possible in matches:
|
|
if string == possible:
|
|
return string, 1
|
|
if possible.startswith(string) or possible.lstrip("[{\\^_`|}]").startswith(string):
|
|
bestmatch = possible
|
|
num_matches += 1
|
|
if num_matches != 1:
|
|
return None, num_matches
|
|
else:
|
|
return bestmatch, 1
|
|
|
|
#wrapper around complete_match() used for roles
|
|
def get_victim(cli, nick, victim, in_chan, self_in_list=False, bot_in_list=False):
|
|
chan = botconfig.CHANNEL if in_chan else nick
|
|
if not victim:
|
|
reply(cli, nick, chan, messages["not_enough_parameters"], private=True)
|
|
return
|
|
pl = [x for x in list_players() if x != nick or self_in_list]
|
|
pll = [x.lower() for x in pl]
|
|
|
|
if bot_in_list: # for villagergame
|
|
pl.append(botconfig.NICK)
|
|
pll.append(botconfig.NICK.lower())
|
|
|
|
tempvictim, num_matches = complete_match(victim.lower(), pll)
|
|
if not tempvictim:
|
|
#ensure messages about not being able to act on yourself work
|
|
if num_matches == 0 and nick.lower().startswith(victim.lower()):
|
|
return nick
|
|
reply(cli, nick, chan, messages["not_playing"].format(victim), private=True)
|
|
return
|
|
return pl[pll.index(tempvictim)] #convert back to normal casing
|
|
|
|
# wrapper around complete_match() used for any nick on the channel
|
|
def get_nick(cli, nick):
|
|
ul = [x for x in var.USERS]
|
|
ull = [x.lower() for x in var.USERS]
|
|
lnick, num_matches = complete_match(nick.lower(), ull)
|
|
if not lnick:
|
|
return None
|
|
return ul[ull.index(lnick)]
|
|
|
|
def pastebin_tb(cli, msg, exc):
|
|
try:
|
|
bot_id = re.sub(r"[^A-Za-z0-9-]", "-", botconfig.NICK)
|
|
bot_id = re.sub(r"--+", "-", bot_id)
|
|
bot_id = re.sub(r"^-+|-+$", "", bot_id)
|
|
|
|
rand_id = "".join(random.sample(string.ascii_letters + string.digits, 8))
|
|
|
|
api_url = "https://ptpb.pw/~{0}-error-{1}".format(bot_id, rand_id)
|
|
|
|
req = urllib.request.Request(api_url, urllib.parse.urlencode({
|
|
"c": exc, # contents
|
|
"s": 86400 # expiry (seconds)
|
|
}).encode("utf-8", "replace"))
|
|
|
|
req.add_header("Accept", "application/json")
|
|
resp = urllib.request.urlopen(req)
|
|
data = json.loads(resp.read().decode("utf-8"))
|
|
url = data["url"] + "/pytb"
|
|
except Exception:
|
|
# Exception is already printed before calling this function, don't print twice
|
|
cli.msg(botconfig.DEV_CHANNEL, msg + " (Unable to pastebin traceback; please check the console.)")
|
|
else:
|
|
cli.msg(botconfig.DEV_CHANNEL, " ".join((msg, url)))
|
|
|
|
class InvalidModeException(Exception): pass
|
|
|
|
# vim: set sw=4 expandtab:
|