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": traceback.format_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"] + "/py3tb" 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: