diff --git a/messages/en.json b/messages/en.json index 78164ad..e65a5fc 100644 --- a/messages/en.json +++ b/messages/en.json @@ -596,7 +596,6 @@ "admin_forced_game": "A game mode has already been forced by an admin.", "vote_game_fail": "You can't vote for that game mode.", "fsend_usage": "Usage: {0}{1} ", - "wrong_channel": "You have to be in {0} to use this command.", "invalid_fsend_permissions": "You do not have permission to message this user or channel.", "temp_invalid_perms": "You are not allowed to use that command right now.", "fgame_success": "\u0002{0}\u0002 has changed the game settings successfully.", diff --git a/src/channels.py b/src/channels.py index d414a38..ed9ef41 100644 --- a/src/channels.py +++ b/src/channels.py @@ -27,7 +27,13 @@ class _States(Enum): def predicate(name): return not name.startswith(tuple(Features["CHANTYPES"])) -get = _channels.__getitem__ +def get(name, *, allow_none=False): + try: + return _channels[name] + except KeyError: + if allow_none: + return None + raise def add(name, cli, key=""): """Add and return a new channel, or an existing one if it exists.""" diff --git a/src/decorators.py b/src/decorators.py index f85e8e0..bcc664a 100644 --- a/src/decorators.py +++ b/src/decorators.py @@ -14,9 +14,10 @@ from oyoyo.parse import parse_nick import botconfig import src.settings as var +from src.dispatcher import MessageDispatcher from src.utilities import * -from src import channels, users, logger, errlog, events from src.messages import messages +from src import channels, users, logger, errlog, events adminlog = logger.logger("audit.log") @@ -181,6 +182,121 @@ class handle_error: with print_traceback(): return self.func(*args, **kwargs) +class command: + def __init__(self, *commands, flag=None, owner_only=False, chan=True, pm=False, + playing=False, silenced=False, phases=(), roles=(), users=None): + + self.commands = frozenset(commands) + self.flag = flag + self.owner_only = owner_only + self.chan = chan + self.pm = pm + self.playing = playing + self.silenced = silenced + self.phases = phases + self.roles = roles + self.users = users # iterable of users that can use the command at any time (should be a mutable object) + self.func = None + self.aftergame = False + self.name = commands[0] + self.alt_allowed = bool(flag or owner_only) + + alias = False + self.aliases = [] + for name in commands: + for func in COMMANDS[name]: + if func.owner_only != owner_only or func.flag != flag: + raise ValueError("unmatching access levels for {0}".format(func.name)) + + COMMANDS[name].append(self) + if name in botconfig.ALLOWED_ALT_CHANNELS_COMMANDS: + self.alt_allowed = True + if name in getattr(botconfig, "OWNERS_ONLY_COMMANDS", ()): + self.owner_only = True + if alias: + self.aliases.append(name) + alias = True + + def __call__(self, func): + if isinstance(func, command): + func = func.func + self.func = func + self.__doc__ = func.__doc__ + return self + + @handle_error + def caller(self, cli, rawnick, chan, rest): + user = users._add(cli, nick=rawnick) # FIXME + + if users.equals(chan, users.Bot.nick): # PM + target = users.Bot + else: + target = channels.add(chan, cli) + + dispatcher = MessageDispatcher(user, target) + + if (not self.pm and dispatcher.private) or (not self.chan and dispatcher.public): + return # channel or PM command that we don't allow + + if dispatcher.public and target is not channels.Main and not (self.flag or self.owner_only): + if "" in self.commands or not self.alt_allowed: + return # commands not allowed in alt channels + + if "" in self.commands: + return self.func(var, dispatcher, rest) + + if self.phases and var.PHASE not in self.phases: + return + + if self.playing and (user.nick not in list_players() or user.nick in var.DISCONNECTED): # FIXME: Need to change this once list_players() / var.DISCONNECTED use User instances + return + + for role in self.roles: + if user.nick in var.ROLES[role]: # FIXME: Need to change this once var.ROLES[role] holds User instances + break + else: + if (self.users is not None and user not in self.users) or self.roles: + return + + if self.silenced and user.nick in var.SILENCED: # FIXME: Need to change this once var.SILENCED holds User instances + dispatcher.pm(messages["silenced"]) + return + + if self.roles or (self.users is not None and user in self.users): + return self.func(var, dispatcher, rest) # don't check restrictions for role commands + + if self.owner_only: + if user.is_owner(): + adminlog(chan, rawnick, self.name, rest) + return self.func(var, dispatcher, rest) + + dispatcher.pm(messages["not_owner"]) + return + + temp = user.lower() + + flags = var.FLAGS[temp.rawnick] + var.FLAGS_ACCS[temp.account] # TODO: add flags handling to User + + if self.flag and (user.is_admin() or user.is_owner()): + adminlog(chan, rawnick, self.name, rest) + return self.func(var, dispatcher, rest) + + denied_commands = var.DENY[temp.rawnick] | var.DENY_ACCS[temp.account] # TODO: add denied commands handling to User + + if self.commands & denied_commands: + dispatcher.pm(messages["invalid_permissions"]) + return + + if self.flag: + if self.flag in flags: + adminlog(chan, rawnick, self.name, rest) + return self.func(var, dispatcher, rest) + + dispatcher.pm(messages["not_an_admin"]) + return + + return self.func(var, dispatcher, rest) + class cmd: def __init__(self, *cmds, raw_nick=False, flag=None, owner_only=False, chan=True, pm=False, playing=False, silenced=False, @@ -222,8 +338,11 @@ class cmd: return self @handle_error - def caller(self, *args): - largs = list(args) + def caller(self, cli, rawnick, chan, rest): + if users.equals(chan, users.Bot.nick): + chan = users.parse_rawnick_as_dict(rawnick)["nick"] + + largs = [cli, rawnick, chan, rest] cli, rawnick, chan, rest = largs nick, mode, ident, host = parse_nick(rawnick) diff --git a/src/dispatcher.py b/src/dispatcher.py new file mode 100644 index 0000000..54237fd --- /dev/null +++ b/src/dispatcher.py @@ -0,0 +1,47 @@ +from src import channels, users +from src import settings as var + +from src.utilities import list_players + +class MessageDispatcher: + """Dispatcher class for raw IRC messages.""" + + def __init__(self, source, target): + self.source = source + self.target = target + + @property + def private(self): + return self.target is users.Bot + + @property + def public(self): + return self.target is not users.Bot + + def pm(self, *messages, **kwargs): + """Send a private message or notice to the sender.""" + kwargs.setdefault("notice", self.public) + self.source.send(*messages, **kwargs) + + def send(self, *messages, **kwargs): + """Send a message to the channel or a private message.""" + if self.private: + self.pm(*messages, **kwargs) + else: + self.target.send(*messages, **kwargs) + + def reply(self, *messages, prefix_nick=False, **kwargs): + """Reply to the user, either in channel or privately.""" + first = "" + if prefix_nick: + first = "{0}: ".format(self.source.nick) + if self.private: + self.source.send(*messages, **kwargs) + elif (self.target is channels.Main and + ((self.source not in list_players() and var.PHASE in var.GAME_PHASES) or + (var.DEVOICE_DURING_NIGHT and var.PHASE == "night"))): # FIXME + kwargs.setdefault("notice", True) + self.source.send(*messages, **kwargs) + else: + kwargs.setdefault("first", first) + self.target.send(*messages, **kwargs) diff --git a/src/gamemodes.py b/src/gamemodes.py index ab95d7d..49b3eec 100644 --- a/src/gamemodes.py +++ b/src/gamemodes.py @@ -10,7 +10,7 @@ import src.settings as var from src.utilities import * from src.messages import messages from src.decorators import handle_error -from src import events +from src import events, channels, users def game_mode(name, minp, maxp, likelihood = 0): def decor(c): @@ -1186,23 +1186,24 @@ class MaelstromMode(GameMode): if not var.ACCOUNTS_ONLY: self.DEAD_HOSTS.add(var.USERS[nick]["host"].lower()) - def on_join(self, evt, cli, var, nick, chan, rest, forced=False): - if var.PHASE != "day" or (nick != chan and chan != botconfig.CHANNEL): + def on_join(self, evt, var, wrapper, message, forced=False): + if var.PHASE != "day" or (wrapper.public and wrapper.target is not channels.Main): return - if (irc_lower(nick) in (irc_lower(x) for x in var.ALL_PLAYERS) or - irc_lower(var.USERS[nick]["account"]) in self.DEAD_ACCOUNTS or - var.USERS[nick]["host"].lower() in self.DEAD_HOSTS): - cli.notice(nick, messages["maelstrom_dead"]) + temp = wrapper.source.lower() + if (temp.nick in (irc_lower(x) for x in var.ALL_PLAYERS) or # FIXME + temp.account in self.DEAD_ACCOUNTS or + temp.host in self.DEAD_HOSTS): + wrapper.pm(messages["maelstrom_dead"]) return - if not forced and evt.data["join_player"](cli, nick, botconfig.CHANNEL, sanity=False): - self._on_join(cli, var, nick, chan) + if not forced and evt.data["join_player"](var, type(wrapper)(wrapper.source, channels.Main), sanity=False): + self._on_join(var, wrapper) evt.prevent_default = True elif forced: # in fjoin, handle this differently jp = evt.data["join_player"] - evt.data["join_player"] = lambda cli, nick, chan, who=None, forced=False: jp(cli, nick, chan, who=who, forced=forced, sanity=False) and self._on_join(cli, var, nick, chan) + evt.data["join_player"] = lambda var, wrapper, who=None, forced=False: jp(var, wrapper, who=who, forced=forced, sanity=False) and self._on_join(var, wrapper) - def _on_join(self, cli, var, nick, chan): + def _on_join(self, var, wrapper): role = random.choice(self.roles) lpl = len(list_players()) + 1 @@ -1232,31 +1233,30 @@ class MaelstromMode(GameMode): elif role == "succubus": lsuccubi += 1 - if self.chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, 0, cli, end_game=False): - return self._on_join(cli, var, nick, chan) + if self.chk_win_conditions(lpl, lwolves, lcubs, lrealwolves, lmonsters, ldemoniacs, ltraitors, lpipers, lsuccubi, 0, wrapper.client, end_game=False): + return self._on_join(var, wrapper) - var.ROLES[role].add(nick) - var.ORIGINAL_ROLES[role].add(nick) - var.FINAL_ROLES[nick] = role - var.LAST_SAID_TIME[nick] = datetime.now() - if nick in var.USERS: - var.PLAYERS[nick] = var.USERS[nick] + var.ROLES[role].add(wrapper.source.nick) + var.ORIGINAL_ROLES[role].add(wrapper.source.nick) + var.FINAL_ROLES[wrapper.source.nick] = role + var.LAST_SAID_TIME[wrapper.source.nick] = datetime.now() + if wrapper.source.nick in var.USERS: + var.PLAYERS[wrapper.source.nick] = var.USERS[wrapper.source.nick] if role == "doctor": lpl = len(list_players()) - var.DOCTORS[nick] = math.ceil(var.DOCTOR_IMMUNIZATION_MULTIPLIER * lpl) + var.DOCTORS[wrapper.source.nick] = math.ceil(var.DOCTOR_IMMUNIZATION_MULTIPLIER * lpl) # let them know their role - # FIXME: this is fugly from src.decorators import COMMANDS - COMMANDS["myrole"][0].caller(cli, nick, chan, "") + COMMANDS["myrole"][0].caller(wrapper.source.client, wrapper.source.nick, wrapper.target.name, "") # FIXME: New/old API # if they're a wolfchat role, alert the other wolves if role in var.WOLFCHAT_ROLES: - relay_wolfchat_command(cli, nick, messages["wolfchat_new_member"].format(nick, role), var.WOLFCHAT_ROLES, is_wolf_command=True, is_kill_command=True) + relay_wolfchat_command(wrapper.source.client, wrapper.source.nick, messages["wolfchat_new_member"].format(wrapper.source.nick, role), var.WOLFCHAT_ROLES, is_wolf_command=True, is_kill_command=True) # TODO: make this part of !myrole instead, no reason we can't give out wofllist in that wolves = list_players(var.WOLFCHAT_ROLES) pl = list_players() random.shuffle(pl) - pl.remove(nick) + pl.remove(wrapper.source.nick) for i, player in enumerate(pl): prole = get_role(player) if prole in var.WOLFCHAT_ROLES: @@ -1266,7 +1266,7 @@ class MaelstromMode(GameMode): pl[i] = "\u0002{0}\u0002 ({1}{2})".format(player, cursed, prole) elif player in var.ROLES["cursed villager"]: pl[i] = player + " (cursed)" - pm(cli, nick, "Players: " + ", ".join(pl)) + wrapper.pm("Players: " + ", ".join(pl)) def role_attribution(self, evt, cli, var, chk_win_conditions, villagers): self.chk_win_conditions = chk_win_conditions diff --git a/src/handler.py b/src/handler.py index a30357b..45f2ec1 100644 --- a/src/handler.py +++ b/src/handler.py @@ -15,22 +15,19 @@ def on_privmsg(cli, rawnick, chan, msg, *, notice=False): if notice and "!" not in rawnick or not rawnick: # server notice; we don't care about those return - if not users.equals(chan, botconfig.NICK) and botconfig.IGNORE_HIDDEN_COMMANDS and not chan.startswith(tuple(hooks.Features["CHANTYPES"])): + if not users.equals(chan, users.Bot.nick) and botconfig.IGNORE_HIDDEN_COMMANDS and not chan.startswith(tuple(hooks.Features["CHANTYPES"])): return - if (notice and ((not users.equals(chan, botconfig.NICK) and not botconfig.ALLOW_NOTICE_COMMANDS) or - (users.equals(chan, botconfig.NICK) and not botconfig.ALLOW_PRIVATE_NOTICE_COMMANDS))): + if (notice and ((not users.equals(chan, users.Bot.nick) and not botconfig.ALLOW_NOTICE_COMMANDS) or + (users.equals(chan, users.Bot.nick) and not botconfig.ALLOW_PRIVATE_NOTICE_COMMANDS))): return # not allowed in settings - if users.equals(chan, botconfig.NICK): - chan = users.parse_rawnick_as_dict(rawnick)["nick"] - for fn in decorators.COMMANDS[""]: fn.caller(cli, rawnick, chan, msg) phase = var.PHASE for x in list(decorators.COMMANDS.keys()): - if chan != users.parse_rawnick_as_dict(rawnick)["nick"] and not msg.lower().startswith(botconfig.CMD_CHAR): + if not users.equals(chan, users.Bot.nick) and not msg.lower().startswith(botconfig.CMD_CHAR): break # channel message but no prefix; ignore if msg.lower().startswith(botconfig.CMD_CHAR+x): h = msg[len(x)+len(botconfig.CMD_CHAR):] diff --git a/src/roles/dullahan.py b/src/roles/dullahan.py index c280266..6353b97 100644 --- a/src/roles/dullahan.py +++ b/src/roles/dullahan.py @@ -55,13 +55,13 @@ def dullahan_retract(cli, nick, chan, rest): pm(cli, nick, messages["retracted_kill"]) @event_listener("player_win") -def on_player_win(evt, cli, var, nick, role, winner, survived): +def on_player_win(evt, var, user, role, winner, survived): if role != "dullahan": return alive = set(list_players()) - if nick in var.ENTRANCED: + if user.nick in var.ENTRANCED: alive -= var.ROLES["succubus"] - if not TARGETS[nick] & alive: + if not TARGETS[user.nick] & alive: evt.data["iwon"] = True @event_listener("del_player") @@ -222,7 +222,7 @@ def on_myrole(evt, cli, var, nick): evt.data["messages"].append(messages["dullahan_targets_dead"]) @event_listener("revealroles_role") -def on_revealroles_role(evt, cli, var, nickname, role): +def on_revealroles_role(evt, var, wrapper, nickname, role): if role == "dullahan" and nickname in TARGETS: targets = TARGETS[nickname] - var.DEAD if targets: diff --git a/src/roles/shaman.py b/src/roles/shaman.py index da8e258..2d31f3b 100644 --- a/src/roles/shaman.py +++ b/src/roles/shaman.py @@ -53,7 +53,7 @@ brokentotem = set() # type: Set[str] @cmd("give", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=var.TOTEM_ORDER) @cmd("totem", chan=False, pm=True, playing=True, silenced=True, phases=("night",), roles=var.TOTEM_ORDER) -def totem(cli, nick, chan, rest, prefix="You"): +def totem(cli, nick, chan, rest, prefix="You"): # XXX: The transition_day_begin event needs updating alongside this """Give a totem to a player.""" victim = get_victim(cli, nick, re.split(" +",rest)[0], False, True) if not victim: @@ -299,7 +299,7 @@ def on_chk_decision_lynch5(evt, cli, var, voters): evt.params.del_player(cli, target, True, end_game=False, killer_role="shaman", deadlist=evt.data["deadlist"], original=target, ismain=False) @event_listener("player_win") -def on_player_win(evt, cli, var, splr, rol, winner, survived): +def on_player_win(evt, var, user, rol, winner, survived): if rol == "crazed shaman" and survived and not winner.startswith("@") and singular(winner) not in var.WIN_STEALER_ROLES: evt.data["iwon"] = True @@ -320,7 +320,7 @@ def on_transition_day_begin(evt, cli, var): ps.remove(succubus) if ps: target = random.choice(ps) - totem.func(cli, shaman, shaman, target, messages["random_totem_prefix"]) + totem.func(cli, shaman, shaman, target, messages["random_totem_prefix"]) # XXX: Old API else: LASTGIVEN[shaman] = None elif shaman not in SHAMANS: @@ -594,7 +594,7 @@ def on_myrole(evt, cli, var, nick): evt.data["messages"].append(messages["totem_simple"].format(TOTEMS[nick])) @event_listener("revealroles_role") -def on_revealroles(evt, cli, var, nickname, role): +def on_revealroles(evt, var, wrapper, nickname, role): if role in var.TOTEM_ORDER and nickname in TOTEMS: if nickname in SHAMANS: evt.data["special_case"].append("giving {0} totem to {1}".format(TOTEMS[nickname], SHAMANS[nickname][0])) diff --git a/src/roles/skel.py b/src/roles/skel.py index a7cbc15..899dc31 100644 --- a/src/roles/skel.py +++ b/src/roles/skel.py @@ -7,8 +7,8 @@ from collections import defaultdict import botconfig import src.settings as var from src.utilities import * -from src import debuglog, errlog, plog -from src.decorators import cmd, event_listener +from src import channels, users, debuglog, errlog, plog +from src.decorators import command, event_listener from src.messages import messages from src.events import Event diff --git a/src/roles/vengefulghost.py b/src/roles/vengefulghost.py index 423250f..77294dc 100644 --- a/src/roles/vengefulghost.py +++ b/src/roles/vengefulghost.py @@ -4,79 +4,79 @@ from collections import defaultdict import src.settings as var from src.utilities import * -from src import debuglog, errlog, plog -from src.decorators import cmd, event_listener +from src import channels, users, debuglog, errlog, plog +from src.decorators import command, event_listener from src.messages import messages from src.events import Event KILLS = {} # type: Dict[str, str] -GHOSTS = {} # type: Dict[str, str] +GHOSTS = {} # type: Dict[users.User, str] # temporary holding variable, only non-empty during transition_day # as such, no need to track nick changes, etc. with it drivenoff = {} # type: Dict[str, str] -@cmd("kill", chan=False, pm=True, playing=False, silenced=True, phases=("night",), nicks=GHOSTS) -def vg_kill(cli, nick, chan, rest): +@command("kill", chan=False, pm=True, playing=False, silenced=True, phases=("night",), users=GHOSTS) +def vg_kill(var, wrapper, message): """Take revenge on someone each night after you die.""" - if GHOSTS[nick][0] == "!": + if GHOSTS[wrapper.source][0] == "!": return - victim = get_victim(cli, nick, re.split(" +",rest)[0], False) + victim = get_victim(wrapper.source.client, wrapper.source.nick, re.split(" +", message)[0], False) if not victim: return - if victim == nick: - pm(cli, nick, messages["player_dead"]) + if victim == wrapper.source.nick: + wrapper.pm(messages["player_dead"]) return wolves = list_players(var.WOLFTEAM_ROLES) - if GHOSTS[nick] == "wolves" and victim not in wolves: - pm(cli, nick, messages["vengeful_ghost_wolf"]) + if GHOSTS[wrapper.source] == "wolves" and victim not in wolves: + wrapper.pm(messages["vengeful_ghost_wolf"]) return - elif GHOSTS[nick] == "villagers" and victim in wolves: - pm(cli, nick, messages["vengeful_ghost_villager"]) + elif GHOSTS[wrapper.source] == "villagers" and victim in wolves: + wrapper.pm(messages["vengeful_ghost_villager"]) return orig = victim evt = Event("targeted_command", {"target": victim, "misdirection": True, "exchange": False}) - evt.dispatch(cli, var, "kill", nick, victim, frozenset({"detrimental"})) + evt.dispatch(wrapper.source.client, var, "kill", wrapper.source.nick, victim, frozenset({"detrimental"})) if evt.prevent_default: return victim = evt.data["target"] - KILLS[nick] = victim + KILLS[wrapper.source.nick] = victim msg = messages["wolf_target"].format(orig) - pm(cli, nick, messages["player"].format(msg)) + wrapper.pm(messages["player"].format(msg)) - debuglog("{0} ({1}) KILL: {2} ({3})".format(nick, get_role(nick), victim, get_role(victim))) - chk_nightdone(cli) + debuglog("{0} ({1}) KILL: {2} ({3})".format(wrapper.source.nick, get_role(wrapper.source.nick), victim, get_role(victim))) + chk_nightdone(wrapper.source.client) -@cmd("retract", "r", chan=False, pm=True, playing=False, phases=("night",)) -def vg_retract(cli, nick, chan, rest): +@command("retract", "r", chan=False, pm=True, playing=False, phases=("night",)) +def vg_retract(var, wrapper, message): """Removes a vengeful ghost's kill selection.""" - if nick not in GHOSTS: + if wrapper.source not in GHOSTS: return - if nick in KILLS: - del KILLS[nick] - pm(cli, nick, messages["retracted_kill"]) + if wrapper.source.nick in KILLS: + del KILLS[wrapper.source.nick] + wrapper.pm(messages["retracted_kill"]) @event_listener("list_participants") def on_list_participants(evt, var): - evt.data["pl"].extend([p for p in GHOSTS if GHOSTS[p][0] != "!"]) + evt.data["pl"].extend([p.nick for p in GHOSTS if GHOSTS[p][0] != "!"]) evt.data["pl"].extend([p for p in drivenoff]) @event_listener("player_win", priority=1) -def on_player_win(evt, cli, var, nick, role, winner, survived): +def on_player_win(evt, var, user, role, winner, survived): # alive VG winning is handled in villager.py # extending VG to work with new teams can be done by registering # a listener at priority > 1, importing src.roles.vengefulghost, - # and checking if the nick is in GHOSTS. - if nick in GHOSTS: + # and checking if the user is in GHOSTS. + if user in GHOSTS: evt.data["special"].append("vg activated") - against = GHOSTS[nick] - if GHOSTS[nick][0] == "!": + against = GHOSTS[user] + if against[0] == "!": evt.data["special"].append("vg driven off") against = against[1:] if against == "villagers" and winner == "wolves": @@ -97,14 +97,15 @@ def on_del_player(evt, cli, var, nick, nickrole, nicktpls, death_triggers): del KILLS[h] # extending VG to work with new teams can be done by registering a listener # at priority < 6, importing src.roles.vengefulghost, and setting - # GHOSTS[nick] to something; if that is done then this logic is not run. - if death_triggers and nickrole == "vengeful ghost" and nick not in GHOSTS: + # GHOSTS[user] to something; if that is done then this logic is not run. + user = users._get(nick) # FIXME + if death_triggers and nickrole == "vengeful ghost" and user not in GHOSTS: if evt.params.killer_role in var.WOLFTEAM_ROLES: - GHOSTS[nick] = "wolves" + GHOSTS[user] = "wolves" else: - GHOSTS[nick] = "villagers" - pm(cli, nick, messages["vengeful_turn"].format(GHOSTS[nick])) - debuglog(nick, "(vengeful ghost) TRIGGER", GHOSTS[nick]) + GHOSTS[user] = "villagers" + user.send(messages["vengeful_turn"].format(GHOSTS[user])) + debuglog(nick, "(vengeful ghost) TRIGGER", GHOSTS[user]) @event_listener("rename_player") def on_rename(evt, cli, var, prefix, nick): @@ -118,8 +119,6 @@ def on_rename(evt, cli, var, prefix, nick): KILLS.update(kvp) if prefix in KILLS: del KILLS[prefix] - if prefix in GHOSTS: - GHOSTS[nick] = GHOSTS.pop(prefix) @event_listener("night_acted") def on_acted(evt, cli, var, nick, sender): @@ -132,22 +131,22 @@ def on_transition_day_begin(evt, cli, var): wolves = set(list_players(var.WOLFTEAM_ROLES)) villagers = set(list_players()) - wolves for ghost, target in GHOSTS.items(): - if target[0] == "!" or ghost in var.SILENCED: + if target[0] == "!" or ghost.nick in var.SILENCED: continue - if ghost not in KILLS: + if ghost.nick not in KILLS: choice = set() if target == "wolves": choice = wolves.copy() elif target == "villagers": choice = villagers.copy() evt = Event("vg_kill", {"pl": choice}) - evt.dispatch(cli, var, ghost, target) + evt.dispatch(var, ghost, target) choice = evt.data["pl"] # roll this into the above event once succubus is split off - if ghost in var.ENTRANCED: + if ghost.nick in var.ENTRANCED: choice -= var.ROLES["succubus"] if choice: - KILLS[ghost] = random.choice(list(choice)) + KILLS[ghost.nick] = random.choice(list(choice)) @event_listener("transition_day", priority=2) def on_transition_day(evt, cli, var): @@ -159,35 +158,35 @@ def on_transition_day(evt, cli, var): @event_listener("transition_day", priority=3.01) def on_transition_day3(evt, cli, var): for k, d in list(KILLS.items()): - if GHOSTS[k] == "villagers": + if GHOSTS[users._get(k)] == "villagers": evt.data["killers"][d].remove(k) evt.data["killers"][d].insert(0, k) @event_listener("transition_day", priority=6.01) def on_transition_day6(evt, cli, var): for k, d in list(KILLS.items()): - if GHOSTS[k] == "villagers" and k in evt.data["killers"][d]: + if GHOSTS[users._get(k)] == "villagers" and k in evt.data["killers"][d]: evt.data["killers"][d].remove(k) evt.data["killers"][d].insert(0, k) # important, otherwise our del_player listener messages the vg del KILLS[k] -@event_listener("retribution_kill") +@event_listener("retribution_kill") # FIXME: This function, and all of the event def on_retribution_kill(evt, cli, var, victim, orig_target): t = evt.data["target"] - if t in GHOSTS: - drivenoff[t] = GHOSTS[t] - GHOSTS[t] = "!" + GHOSTS[t] + if users._get(t) in GHOSTS: + drivenoff[t] = GHOSTS[users._get(t)] + GHOSTS[users._get(t)] = "!" + GHOSTS[users._get(t)] evt.data["message"].append(messages["totem_banish"].format(victim, t)) evt.data["target"] = None @event_listener("get_participant_role") def on_get_participant_role(evt, var, nick): - if nick in GHOSTS: + if users._get(nick) in GHOSTS: # FIXME if nick in drivenoff: against = drivenoff[nick] else: - against = GHOSTS[nick] + against = GHOSTS[users._get(nick)] if against == "villagers": evt.data["role"] = "wolf" elif against == "wolves": @@ -196,7 +195,7 @@ def on_get_participant_role(evt, var, nick): @event_listener("chk_nightdone") def on_chk_nightdone(evt, cli, var): evt.data["actedcount"] += len(KILLS) - evt.data["nightroles"].extend([p for p in GHOSTS if GHOSTS[p][0] != "!"]) + evt.data["nightroles"].extend([p.nick for p in GHOSTS if GHOSTS[p][0] != "!"]) @event_listener("transition_night_end", priority=2) def on_transition_night_end(evt, cli, var): @@ -215,27 +214,28 @@ def on_transition_night_end(evt, cli, var): random.shuffle(pl) - if v_ghost in var.PLAYERS and not is_user_simple(v_ghost): - pm(cli, v_ghost, messages["vengeful_ghost_notify"].format(who)) + if not v_ghost.prefers_simple(): + v_ghost.send(messages["vengeful_ghost_notify"].format(who)) else: - pm(cli, v_ghost, messages["vengeful_ghost_simple"]) - pm(cli, v_ghost, who.capitalize() + ": " + ", ".join(pl)) - debuglog("GHOST: {0} (target: {1}) - players: {2}".format(v_ghost, who, ", ".join(pl))) + v_ghost.send(messages["vengeful_ghost_simple"]) + v_ghost.send(who.capitalize() + ": " + ", ".join(pl)) + debuglog("GHOST: {0} (target: {1}) - players: {2}".format(v_ghost.nick, who, ", ".join(pl))) @event_listener("myrole") def on_myrole(evt, cli, var, nick): - if nick in GHOSTS: + user = users._get(nick) + if user in GHOSTS: evt.prevent_default = True - if GHOSTS[nick][0] != "!": - pm(cli, nick, messages["vengeful_role"].format(GHOSTS[nick])) + if GHOSTS[user][0] != "!": + user.send(messages["vengeful_role"].format(GHOSTS[user])) @event_listener("revealroles") -def on_revealroles(evt, cli, var): +def on_revealroles(evt, var, wrapper): if GHOSTS: glist = [] for ghost, team in GHOSTS.items(): dead = "driven away, " if team[0] == "!" else "" - glist.append("{0} ({1}against {2})".format(ghost, dead, team.lstrip("!"))) + glist.append("{0} ({1}against {2})".format(ghost.nick, dead, team.lstrip("!"))) evt.data["output"].append("\u0002dead vengeful ghost\u0002: {0}".format(", ".join(glist))) @event_listener("begin_day") diff --git a/src/roles/villager.py b/src/roles/villager.py index 927fd85b..67aa177 100644 --- a/src/roles/villager.py +++ b/src/roles/villager.py @@ -33,7 +33,7 @@ def on_transition_night_end(evt, cli, var): # No listeners should register before this one # This sets up the initial state, based on village/wolfteam/neutral affiliation @event_listener("player_win", priority=0) -def on_player_win(evt, cli, var, nick, role, winner, survived): +def on_player_win(evt, var, user, role, winner, survived): # init won/iwon to False evt.data["won"] = False evt.data["iwon"] = False diff --git a/src/roles/wildchild.py b/src/roles/wildchild.py index 4b686c9..7a29aeb 100644 --- a/src/roles/wildchild.py +++ b/src/roles/wildchild.py @@ -145,7 +145,7 @@ def on_transition_night_end(evt, cli, var): pm(cli, child, messages["child_simple"]) @event_listener("revealroles_role") -def on_revealroles_role(evt, cli, var, nick, role): +def on_revealroles_role(evt, var, wrapper, nick, role): if role == "wild child": if nick in IDOLS: evt.data["special_case"].append("picked {0} as idol".format(IDOLS[nick])) diff --git a/src/users.py b/src/users.py index 966c085..24ab1ae 100644 --- a/src/users.py +++ b/src/users.py @@ -104,8 +104,14 @@ def _add(cli, *, nick, ident=None, host=None, realname=None, account=None): new = cls(cli, nick, ident, host, realname, account) - if new is not Bot and new.ident is not None and new.host is not None: - _users.add(new) + if new is not Bot: + try: + hash(new) + except ValueError: + pass + else: + _users.add(new) + return new def add(nick, **blah): # backwards-compatible API @@ -246,7 +252,7 @@ class User(IRCContext): return True for hostmask in hosts: - if match_hostmask(hostmask, self.nick, self.ident, self.host): + if self.match_hostmask(hostmask): return True return False @@ -268,7 +274,7 @@ class User(IRCContext): return True for hostmask in hosts: - if match_hostmask(hostmask, self.nick, self.ident, self.host): + if self.match_hostmask(hostmask): return True except AttributeError: pass diff --git a/src/wolfgame.py b/src/wolfgame.py index 98bb79b..b429a38 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -46,10 +46,11 @@ import botconfig import src import src.settings as var from src.utilities import * -from src import db, decorators, events, channels, users, hooks, logger, proxy, debuglog, errlog, plog -from src.decorators import cmd, hook, handle_error, event_listener, COMMANDS +from src import db, events, channels, users, hooks, logger, proxy, debuglog, errlog, plog +from src.decorators import command, cmd, hook, handle_error, event_listener, COMMANDS from src.messages import messages from src.warnings import * +from src.context import IRCContext # done this way so that events is accessible in !eval (useful for debugging) Event = events.Event @@ -135,12 +136,12 @@ def connect_callback(): signal.signal(signal.SIGINT, signal.SIG_DFL) if signum in (signal.SIGINT, signal.SIGTERM): - forced_exit.func(cli, "", botconfig.CHANNEL, "") + forced_exit.func(cli, "", botconfig.CHANNEL, "") # XXX: Old API elif signum == SIGUSR1: - restart_program.func(cli, "", botconfig.CHANNEL, "") + restart_program.func(cli, "", botconfig.CHANNEL, "") # XXX: Old API elif signum == SIGUSR2: plog("Scheduling aftergame restart") - aftergame.func(cli, "", botconfig.CHANNEL, "frestart") + aftergame.func(cli, "", botconfig.CHANNEL, "frestart") # XXX: Old API signal.signal(signal.SIGINT, sighandler) signal.signal(signal.SIGTERM, sighandler) @@ -360,7 +361,7 @@ def refreshdb(cli, nick, chan, rest): reply(cli, nick, chan, "Done.") @cmd("die", "bye", "fdie", "fbye", flag="D", pm=True) -def forced_exit(cli, nick, chan, rest): +def forced_exit(cli, nick, chan, rest): # XXX: sighandler (top of file) also needs updating alongside this one """Forces the bot to close.""" args = rest.split() @@ -413,7 +414,7 @@ def _restart_program(cli, mode=None): @cmd("restart", "frestart", flag="D", pm=True) -def restart_program(cli, nick, chan, rest): +def restart_program(cli, nick, chan, rest): # XXX: sighandler (top of file) also needs updating alongside this one """Restarts the bot.""" args = rest.split() @@ -791,7 +792,7 @@ def toggle_altpinged_status(nick, value, old=None): var.PING_IF_NUMS[old].discard(hostmask) @handle_error -def join_timer_handler(cli): +def join_timer_handler(): with var.WARNING_LOCK: var.PINGING_IFS = True to_ping = [] @@ -830,7 +831,7 @@ def join_timer_handler(cli): var.PINGING_IFS = False return - @hook("whoreply", hookid=387) + @hook("whoreply", hookid=387) # FIXME: Use events def ping_altpingers_noacc(cli, bot_server, bot_nick, chan, ident, host, server, nick, status, hopcount_gecos): if ("G" in status or is_user_stasised(nick) or not var.PINGING_IFS or nick == bot_nick or nick in pl): @@ -865,7 +866,7 @@ def join_timer_handler(cli): var.PINGED_ALREADY.add(hostmask) @hook("endofwho", hookid=387) - def fetch_altpingers(*stuff): + def fetch_altpingers(cli, *stuff): # fun fact: if someone joined 10 seconds after someone else, the bot would break. # effectively, the join would delete join_pinger from var.TIMERS and this function # here would be reached before it was created again, thus erroring and crashing. @@ -882,11 +883,7 @@ def join_timer_handler(cli): cli.msg(botconfig.CHANNEL, msg) - # FIXME - if not var.DISABLE_ACCOUNTS: - cli.who(botconfig.CHANNEL, "%tcuihsnfdlar,") - else: - cli.who(botconfig.CHANNEL) + channels.Main.who() def get_deadchat_pref(nick): if users.exists(nick): @@ -991,8 +988,8 @@ def deadchat_pref(cli, nick, chan, rest): reply(cli, nick, chan, msg, private=True) -@cmd("join", "j", pm=True) -def join(cli, nick, chan, rest): +@command("join", "j", pm=True) +def join(var, wrapper, message): """Either starts a new game of Werewolf or joins an existing game that has not started yet.""" # keep this and the event in fjoin() in sync evt = Event("join", { @@ -1000,72 +997,57 @@ def join(cli, nick, chan, rest): "join_deadchat": join_deadchat, "vote_gamemode": vote_gamemode }) - if not evt.dispatch(cli, var, nick, chan, rest, forced=False): + if not evt.dispatch(var, wrapper, message, forced=False): return if var.PHASE in ("none", "join"): - if chan == nick: + if wrapper.private: return if var.ACCOUNTS_ONLY: - if users.exists(nick) and (not users.get(nick).account or users.get(nick).account == "*"): - cli.notice(nick, messages["not_logged_in"]) + if users._get(nick).account is None: # FIXME + wrapper.pm(messages["not_logged_in"]) return - if evt.data["join_player"](cli, nick, chan) and rest: - evt.data["vote_gamemode"](cli, nick, chan, rest.lower().split()[0], False) + if evt.data["join_player"](var, wrapper) and message: + evt.data["vote_gamemode"](var, wrapper, message.lower().split()[0], doreply=False) else: # join deadchat - if chan == nick and nick != botconfig.NICK: - evt.data["join_deadchat"](cli, nick) + if wrapper.private and wrapper.source is not wrapper.target: + evt.data["join_deadchat"](var, wrapper) -def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): +def join_player(var, wrapper, who=None, forced=False, *, sanity=True): if who is None: - who = player + who = wrapper.source pl = list_players() - if chan != botconfig.CHANNEL: + if wrapper.target is not channels.Main: return False - if users.exists(player): - ident = irc_lower(users.get(player).ident) - host = users.get(player).host.lower() - acc = irc_lower(users.get(player).account) - hostmask = player + "!" + ident + "@" + host - elif is_fake_nick(player) and botconfig.DEBUG_MODE: - # fakenick - ident = None - host = None - acc = None - hostmask = None - else: - return False # Not normal - if not acc or acc == "*" or var.DISABLE_ACCOUNTS: - acc = None - - stasis = is_user_stasised(player) + stasis = wrapper.source.stasis_count() if stasis > 0: if forced and stasis == 1: - decrement_stasis(player) + decrement_stasis(wrapper.source.nick) else: - cli.notice(who, messages["stasis"].format( - "you are" if player == who else player + " is", stasis, - "s" if stasis != 1 else "")) + who.send(messages["stasis"].format( + "you are" if wrapper.source is who else wrapper.source.nick + " is", stasis, + "s" if stasis != 1 else ""), notice=True) return False + temp = wrapper.source.lower() + # don't check unacked warnings on fjoin - if who == player and db.has_unacknowledged_warnings(acc, hostmask): - cli.notice(player, messages["warn_unacked"]) + if wrapper.source is who and db.has_unacknowledged_warnings(temp.account, temp.rawnick): + wrapper.pm(messages["warn_unacked"]) return False - cmodes = [("+v", player)] + cmodes = [("+v", wrapper.source.nick)] if var.PHASE == "none": - if var.AUTO_TOGGLE_MODES and users.exists(player) and users.get(player).modes: - for mode in users.get(player).modes: - cmodes.append(("-"+mode, player)) - users.get(player).moded.update(users.get(player).modes) - users.get(player).modes = set() - mass_mode(cli, cmodes, []) - var.ROLES["person"].add(player) - var.ALL_PLAYERS.append(player) + if var.AUTO_TOGGLE_MODES and users.get(player).modes: # FIXME: Need to properly handle mode changes (whole block) + for mode in users.get(wrapper.source.nick).modes: + cmodes.append(("-"+mode, wrapper.source.nick)) + users.get(wrapper.source.nick).moded.update(users.get(wrapper.source.nick).modes) + users.get(wrapper.source.nick).modes.clear() + var.ROLES["person"].add(wrapper.source.nick) # FIXME: Need to store Users, not nicks + var.ALL_PLAYERS.append(wrapper.source.nick) var.PHASE = "join" with var.WAIT_TB_LOCK: var.WAIT_TB_TOKENS = var.WAIT_TB_INIT @@ -1073,12 +1055,12 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): var.GAME_ID = time.time() var.PINGED_ALREADY_ACCS = set() var.PINGED_ALREADY = set() - if host: - var.JOINED_THIS_GAME.add(ident + "@" + host) - if acc: - var.JOINED_THIS_GAME_ACCS.add(acc) + if wrapper.source.userhost: + var.JOINED_THIS_GAME.add(wrapper.source.userhost) + if wrapper.source.account: + var.JOINED_THIS_GAME_ACCS.add(wrapper.source.account) var.CAN_START_TIME = datetime.now() + timedelta(seconds=var.MINIMUM_WAIT) - cli.msg(chan, messages["new_game"].format(player, botconfig.CMD_CHAR)) + wrapper.send(messages["new_game"].format(wrapper.source.nick, botconfig.CMD_CHAR)) # Set join timer if var.JOIN_TIME_LIMIT > 0: @@ -1087,52 +1069,50 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): t.daemon = True t.start() - elif player in pl: - cli.notice(who, messages["already_playing"].format("You" if who == player else "They")) + elif wrapper.source.nick in pl: # FIXME: To fix when everything returns Users + who.send(messages["already_playing"].format("You" if who is wrapper.source else "They"), notice=True) # if we're not doing insane stuff, return True so that one can use !join to vote for a game mode # even if they are already joined. If we ARE doing insane stuff, return False to indicate that # the player was not successfully joined by this call. return sanity elif len(pl) >= var.MAX_PLAYERS: - cli.notice(who, messages["too_many_players"]) + who.send(messages["too_many_players"], notice=True) return False elif sanity and var.PHASE != "join": - cli.notice(who, messages["game_already_running"]) + who.send(messages["game_already_running"], notice=True) return False else: - if acc is not None and not botconfig.DEBUG_MODE: - for user in pl: - if irc_lower(users.get(user).account) == acc: + if not botconfig.DEBUG_MODE: + for nick in pl: + if users._get(nick).lower().account == temp.account: # FIXME msg = messages["account_already_joined"] - if who == player: - cli.notice(who, msg.format(user, "your", messages["join_swap_instead"].format(botconfig.CMD_CHAR))) + if who is wrapper.source: + who.send(msg.format(user, "your", messages["join_swap_instead"].format(botconfig.CMD_CHAR)), notice=True) else: - cli.notice(who, msg.format(user, "their", "")) + who.send(msg.format(user, "their", ""), notice=True) return - var.ALL_PLAYERS.append(player) - if not is_fake_nick(player) or not botconfig.DEBUG_MODE: - if var.AUTO_TOGGLE_MODES and users.get(player).modes: - for mode in var.USERS[player]["modes"]: - cmodes.append(("-"+mode, player)) - users.get(player).moded.update(users.get(player).modes) - users.get(player).modes = set() - mass_mode(cli, cmodes, []) - cli.msg(chan, messages["player_joined"].format(player, len(pl) + 1)) + var.ALL_PLAYERS.append(wrapper.source.nick) + if not wrapper.source.is_fake or not botconfig.DEBUG_MODE: + if var.AUTO_TOGGLE_MODES and users.get(wrapper.source.nick).modes: + for mode in users.get(wrapper.source.nick).modes: + cmodes.append(("-"+mode, wrapper.source.nick)) + users.get(wrapper.source.nick).moded.update(users.get(wrapper.source.nick).modes) + users.get(wrapper.source.nick).modes.clear() + wrapper.send(messages["player_joined"].format(wrapper.source.nick, len(pl) + 1)) if not sanity: # Abandon Hope All Ye Who Enter Here - leave_deadchat(cli, player) - var.SPECTATING_DEADCHAT.discard(player) - var.SPECTATING_WOLFCHAT.discard(player) + leave_deadchat(wrapper.source.client, wrapper.source.nick) + var.SPECTATING_DEADCHAT.discard(wrapper.source.nick) + var.SPECTATING_WOLFCHAT.discard(wrapper.source.nick) return True - var.ROLES["person"].add(player) - if not is_fake_nick(player): - hostmask = ident + "@" + host - if hostmask not in var.JOINED_THIS_GAME and (not acc or acc not in var.JOINED_THIS_GAME_ACCS): + var.ROLES["person"].add(wrapper.source.nick) + if not wrapper.source.is_fake: + if wrapper.source.userhost not in var.JOINED_THIS_GAME and wrapper.source.account not in var.JOINED_THIS_GAME_ACCS: # make sure this only happens once - var.JOINED_THIS_GAME.add(hostmask) - if acc: - var.JOINED_THIS_GAME_ACCS.add(acc) + var.JOINED_THIS_GAME.add(wrapper.source.userhost) + if wrapper.source.account: + var.JOINED_THIS_GAME_ACCS.add(wrapper.source.account) now = datetime.now() # add var.EXTRA_WAIT_JOIN to wait time @@ -1154,11 +1134,13 @@ def join_player(cli, player, chan, who=None, forced=False, *, sanity=True): if "join_pinger" in var.TIMERS: var.TIMERS["join_pinger"][0].cancel() - t = threading.Timer(10, join_timer_handler, (cli,)) + t = threading.Timer(10, join_timer_handler) var.TIMERS["join_pinger"] = (t, time.time(), 10) t.daemon = True t.start() + channels.Main.mode(*cmodes) + return True @handle_error @@ -1179,8 +1161,8 @@ def kill_join(cli, chan): var.AFTER_FLASTGAME = None -@cmd("fjoin", flag="A") -def fjoin(cli, nick, chan, rest): +@command("fjoin", flag="A") +def fjoin(var, wrapper, message): """Forces someone to join a game.""" # keep this and the event in def join() in sync evt = Event("join", { @@ -1188,24 +1170,26 @@ def fjoin(cli, nick, chan, rest): "join_deadchat": join_deadchat, "vote_gamemode": vote_gamemode }) - if not evt.dispatch(cli, var, nick, chan, rest, forced=True): + if not evt.dispatch(var, wrapper, message, forced=True): return noticed = False fake = False - if not rest.strip(): - evt.data["join_player"](cli, nick, chan, forced=True) + if not message.strip(): + evt.data["join_player"](var, wrapper, forced=True) - for tojoin in re.split(" +",rest): + for tojoin in re.split(" +", message): tojoin = tojoin.strip() if "-" in tojoin: first, hyphen, last = tojoin.partition("-") if first.isdigit() and last.isdigit(): if int(last)+1 - int(first) > var.MAX_PLAYERS - len(list_players()): - cli.msg(chan, messages["too_many_players_to_join"].format(nick)) + wrapper.send(messages["too_many_players_to_join"].format(wrapper.source.nick)) break fake = True for i in range(int(first), int(last)+1): - evt.data["join_player"](cli, str(i), chan, forced=True, who=nick) + user = users._add(wrapper.source.client, nick=str(i)) # FIXME + channels.Dummy.users.add(user) # keep a strong reference to fake users (this will be removed eventually) + evt.data["join_player"](var, type(wrapper)(user, wrapper.target), forced=True, who=wrapper.source) continue if not tojoin: continue @@ -1214,23 +1198,23 @@ def fjoin(cli, nick, chan, rest): if tojoin.lower() not in ull or not var.USERS[ul[ull.index(tojoin.lower())]]["inchan"]: if not is_fake_nick(tojoin) or not botconfig.DEBUG_MODE: if not noticed: # important - cli.msg(chan, nick+messages["fjoin_in_chan"]) + wrapper.send(wrapper.source.nick+messages["fjoin_in_chan"]) noticed = True continue if not is_fake_nick(tojoin): tojoin = ul[ull.index(tojoin.lower())].strip() if not botconfig.DEBUG_MODE and var.ACCOUNTS_ONLY: if not users.get(tojoin).account or users.get(tojoin).account == "*": - cli.notice(nick, messages["account_not_logged_in"].format(tojoin)) + wrapper.pm(messages["account_not_logged_in"].format(tojoin)) return elif botconfig.DEBUG_MODE: fake = True - if tojoin != botconfig.NICK: - evt.data["join_player"](cli, tojoin, chan, forced=True, who=nick) + if tojoin != users.Bot.nick: + evt.data["join_player"](var, type(wrapper)(users._add(wrapper.source.client, nick=tojoin), wrapper.target), forced=True, who=wrapper.source) else: - cli.notice(nick, messages["not_allowed"]) + wrapper.pm(messages["not_allowed"]) if fake: - cli.msg(chan, messages["fjoin_success"].format(nick, len(list_players()))) + wrapper.send(messages["fjoin_success"].format(wrapper.source.nick, len(list_players()))) @cmd("fleave", "fquit", flag="A", pm=True, phases=("join", "day", "night")) def fleave(cli, nick, chan, rest): @@ -2381,7 +2365,7 @@ def stop_game(cli, winner="", abort=False, additional_winners=None, log=True): survived = list_players() if not pentry["dced"]: evt = Event("player_win", {"won": won, "iwon": iwon, "special": pentry["special"]}) - evt.dispatch(cli, var, splr, rol, winner, splr in survived) + evt.dispatch(var, users._get(splr), rol, winner, splr in survived) # FIXME won = evt.data["won"] iwon = evt.data["iwon"] # ensure that it is a) a list, and b) a copy (so it can't be mutated out from under us later) @@ -3757,7 +3741,7 @@ def transition_day(cli, gameid=0): for mm in var.ROLES["matchmaker"]: if mm not in var.MATCHMAKERS: lovers = random.sample(pl, 2) - choose.func(cli, mm, mm, lovers[0] + " " + lovers[1], sendmsg=False) + choose.func(cli, mm, mm, lovers[0] + " " + lovers[1], sendmsg=False) # XXX: Old API pm(cli, mm, messages["random_matchmaker"]) # Reset daytime variables @@ -4653,35 +4637,34 @@ def retract(cli, nick, chan, rest): else: cli.notice(nick, messages["pending_vote"]) -@cmd("shoot", playing=True, silenced=True, phases=("day",)) -def shoot(cli, nick, chan, rest): +@command("shoot", playing=True, silenced=True, phases=("day",)) +def shoot(var, wrapper, message): """Use this to fire off a bullet at someone in the day if you have bullets.""" - - if chan != botconfig.CHANNEL: + if wrapper.target is not channels.Main: return - if nick not in var.GUNNERS.keys(): - cli.notice(nick, messages["no_gun"]) + if wrapper.source.nick not in var.GUNNERS.keys(): + wrapper.pm(messages["no_gun"]) return - elif not var.GUNNERS.get(nick): - cli.notice(nick, messages["no_bullets"]) + elif not var.GUNNERS.get(wrapper.source.nick): + wrapper.pm(messages["no_bullets"]) return - victim = get_victim(cli, nick, re.split(" +",rest)[0], True) + victim = get_victim(wrapper.source.client, wrapper.source.nick, re.split(" +", message)[0], True) if not victim: return - if victim == nick: - cli.notice(nick, messages["gunner_target_self"]) + if victim == wrapper.source.nick: + wrapper.pm(messages["gunner_target_self"]) return # get actual victim - victim = choose_target(nick, victim) + victim = choose_target(wrapper.source.nick, victim) - wolfshooter = nick in list_players(var.WOLFCHAT_ROLES) - var.GUNNERS[nick] -= 1 + wolfshooter = wrapper.source.nick in list_players(var.WOLFCHAT_ROLES) + var.GUNNERS[wrapper.source.nick] -= 1 rand = random.random() - if nick in var.ROLES["village drunk"]: + if wrapper.source.nick in var.ROLES["village drunk"]: chances = var.DRUNK_GUN_CHANCES - elif nick in var.ROLES["sharpshooter"]: + elif wrapper.source.nick in var.ROLES["sharpshooter"]: chances = var.SHARPSHOOTER_GUN_CHANCES else: chances = var.GUN_CHANCES @@ -4698,26 +4681,26 @@ def shoot(cli, nick, chan, rest): if rand <= chances[0] and not (wolfshooter and wolfvictim) and not alwaysmiss: # didn't miss or suicide and it's not a wolf shooting another wolf - cli.msg(chan, messages["shoot_success"].format(nick, victim)) + wrapper.send(messages["shoot_success"].format(wrapper.source.nick, victim)) an = "n" if victimrole.startswith(("a", "e", "i", "o", "u")) else "" if realrole in var.WOLF_ROLES: if var.ROLE_REVEAL == "on": - cli.msg(chan, messages["gunner_victim_wolf_death"].format(victim,an, victimrole)) + wrapper.send(messages["gunner_victim_wolf_death"].format(victim,an, victimrole)) else: # off and team - cli.msg(chan, messages["gunner_victim_wolf_death_no_reveal"].format(victim)) - if not del_player(cli, victim, killer_role = get_role(nick)): + wrapper.send(messages["gunner_victim_wolf_death_no_reveal"].format(victim)) + if not del_player(wrapper.source.client, victim, killer_role=get_role(wrapper.source.nick)): return elif random.random() <= chances[3]: accident = "accidentally " - if nick in var.ROLES["sharpshooter"]: + if wrapper.source.nick in var.ROLES["sharpshooter"]: accident = "" # it's an accident if the sharpshooter DOESN'T headshot :P - cli.msg(chan, messages["gunner_victim_villager_death"].format(victim, accident)) + wrapper.send(messages["gunner_victim_villager_death"].format(victim, accident)) if var.ROLE_REVEAL in ("on", "team"): - cli.msg(chan, messages["gunner_victim_role"].format(an, victimrole)) - if not del_player(cli, victim, killer_role = get_role(nick)): + wrapper.send(messages["gunner_victim_role"].format(an, victimrole)) + if not del_player(wrapper.source.client, victim, killer_role=get_role(wrapper.source.nick)): return else: - cli.msg(chan, messages["gunner_victim_injured"].format(victim)) + wrapper.send(messages["gunner_victim_injured"].format(victim)) var.WOUNDED.add(victim) lcandidates = list(var.VOTES.keys()) for cand in lcandidates: # remove previous vote @@ -4726,16 +4709,16 @@ def shoot(cli, nick, chan, rest): if not var.VOTES.get(cand): del var.VOTES[cand] break - chk_decision(cli) - chk_win(cli) + chk_decision(wrapper.source.client) + chk_win(wrapper.source.client) elif rand <= chances[0] + chances[1]: - cli.msg(chan, messages["gunner_miss"].format(nick)) + wrapper.send(messages["gunner_miss"].format(wrapper.source.nick)) else: if var.ROLE_REVEAL in ("on", "team"): - cli.msg(chan, messages["gunner_suicide"].format(nick, get_reveal_role(nick))) + wrapper.send(messages["gunner_suicide"].format(wrapper.source.nick, get_reveal_role(wrapper.source.nick))) else: - cli.msg(chan, messages["gunner_suicide_no_reveal"].format(nick)) - if not del_player(cli, nick, killer_role = "villager"): # blame explosion on villager's shoddy gun construction or something + wrapper.send(messages["gunner_suicide_no_reveal"].format(wrapper.source.nick)) + if not del_player(wrapper.source.client, wrapper.source.nick, killer_role="villager"): # blame explosion on villager's shoddy gun construction or something return # Someone won. def is_safe(nick, victim): # helper function @@ -4788,7 +4771,7 @@ def consecrate(cli, nick, chan, rest): # (for example, if there was a role that could raise corpses as undead somethings, this would prevent that from working) # regardless if this has any actual effect or not, it still removes the priest from being able to vote from src.roles import vengefulghost - if victim in vengefulghost.GHOSTS: + if users._get(victim) in vengefulghost.GHOSTS: var.SILENCED.add(victim) var.CONSECRATING.add(nick) @@ -4967,7 +4950,7 @@ def hvisit(cli, nick, chan, rest): return if nick == victim: # Staying home (same as calling pass, so call pass) - pass_cmd.func(cli, nick, chan, "") + pass_cmd.func(cli, nick, chan, "") # XXX: Old API return else: victim = choose_target(nick, victim) @@ -5108,7 +5091,7 @@ def bite_cmd(cli, nick, chan, rest): @cmd("pass", chan=False, pm=True, playing=True, phases=("night",), roles=("harlot", "turncoat", "warlock", "succubus")) -def pass_cmd(cli, nick, chan, rest): +def pass_cmd(cli, nick, chan, rest): # XXX: hvisit (3 functions above this one) also needs updating alongside this """Decline to use your special power for that night.""" nickrole = get_role(nick) @@ -5174,7 +5157,7 @@ def change_sides(cli, nick, chan, rest, sendmsg=True): @cmd("choose", chan=False, pm=True, playing=True, phases=("night",), roles=("matchmaker",)) @cmd("match", chan=False, pm=True, playing=True, phases=("night",), roles=("matchmaker",)) -def choose(cli, nick, chan, rest, sendmsg=True): +def choose(cli, nick, chan, rest, sendmsg=True): # XXX: transition_day also needs updating alongside this one """Select two players to fall in love. You may select yourself as one of the lovers.""" if not var.FIRST_NIGHT: return @@ -7069,7 +7052,7 @@ def myrole(cli, nick, chan, rest): pm(cli, nick, message) @cmd("aftergame", "faftergame", flag="D", raw_nick=True, pm=True) -def aftergame(cli, rawnick, chan, rest): +def aftergame(cli, rawnick, chan, rest): # XXX: lastgame (just below this one) and sighandler (top of file) also need updating alongside this one """Schedule a command to be run after the current game.""" nick = parse_nick(rawnick)[0] if not rest.strip(): @@ -7116,7 +7099,7 @@ def flastgame(cli, rawnick, chan, rest): var.ADMIN_TO_PING = nick if rest.strip(): - aftergame.func(cli, rawnick, botconfig.CHANNEL, rest) + aftergame.func(cli, rawnick, botconfig.CHANNEL, rest) # XXX: Old API @cmd("gamestats", "gstats", pm=True) def game_stats(cli, nick, chan, rest): @@ -7159,7 +7142,7 @@ def game_stats(cli, nick, chan, rest): # Attempt to find game stats for the given game size reply(cli, nick, chan, db.get_game_stats(gamemode, gamesize)) -@cmd("playerstats", "pstats", "player", "p", pm=True) +@cmd("playerstats", "pstats", "player", "p", pm=True) # XXX: mystats (just after this) needs updating along this one def player_stats(cli, nick, chan, rest): """Gets the stats for the given player and role or a list of role totals if no role is given.""" if (chan != nick and var.LAST_PSTATS and var.PSTATS_RATE_LIMIT and @@ -7225,49 +7208,48 @@ def my_stats(cli, nick, chan, rest): player_stats.func(cli, nick, chan, " ".join([nick] + rest)) # Called from !game and !join, used to vote for a game mode -def vote_gamemode(cli, nick, chan, gamemode, doreply): +def vote_gamemode(var, wrapper, gamemode, doreply): if var.FGAMED: if doreply: - cli.notice(nick, messages["admin_forced_game"]) + wrapper.pm(messages["admin_forced_game"]) return if gamemode not in var.GAME_MODES.keys(): match, _ = complete_match(gamemode, var.GAME_MODES.keys() - ["roles", "villagergame"] - var.DISABLED_GAMEMODES) if not match: if doreply: - cli.notice(nick, messages["invalid_mode_no_list"].format(gamemode)) + wrapper.pm(messages["invalid_mode_no_list"].format(gamemode)) return gamemode = match if gamemode != "roles" and gamemode != "villagergame" and gamemode not in var.DISABLED_GAMEMODES: - if var.GAMEMODE_VOTES.get(nick) == gamemode: - cli.notice(nick, messages["already_voted_game"].format(gamemode)) + if var.GAMEMODE_VOTES.get(wrapper.source.nick) == gamemode: + wrapper.pm(messages["already_voted_game"].format(gamemode)) else: - var.GAMEMODE_VOTES[nick] = gamemode - cli.msg(chan, messages["vote_game_mode"].format(nick, gamemode)) + var.GAMEMODE_VOTES[wrapper.source.nick] = gamemode + wrapper.send(messages["vote_game_mode"].format(wrapper.source.nick, gamemode)) else: if doreply: - cli.notice(nick, messages["vote_game_fail"]) + wrapper.pm(messages["vote_game_fail"]) -@cmd("game", playing=True, phases=("join",)) -def game(cli, nick, chan, rest): +@command("game", playing=True, phases=("join",)) +def game(var, wrapper, message): """Vote for a game mode to be picked.""" - if rest: - vote_gamemode(cli, nick, chan, rest.lower().split()[0], True) + if message: + vote_gamemode(var, wrapper, message.lower().split()[0], doreply=True) else: gamemodes = ", ".join("\u0002{0}\u0002".format(gamemode) if len(list_players()) in range(var.GAME_MODES[gamemode][1], var.GAME_MODES[gamemode][2]+1) else gamemode for gamemode in var.GAME_MODES.keys() if gamemode != "roles" and gamemode != "villagergame" and gamemode not in var.DISABLED_GAMEMODES) - cli.notice(nick, messages["no_mode_specified"] + gamemodes) + wrapper.pm(messages["no_mode_specified"] + gamemodes) return -@cmd("games", "modes", pm=True) -def show_modes(cli, nick, chan, rest): +@command("games", "modes", pm=True) +def show_modes(var, wrapper, message): """Show the available game modes.""" - msg = messages["available_modes"] modes = "\u0002, \u0002".join(sorted(var.GAME_MODES.keys() - {"roles", "villagergame"} - var.DISABLED_GAMEMODES)) - reply(cli, nick, chan, msg + modes + "\u0002", private=True) + wrapper.pm("{0}{1}\u0002".format(messages["available_modes"], modes)) def game_help(args=""): return (messages["available_mode_setters_help"] + @@ -7359,52 +7341,53 @@ def update(cli, nick, chan, rest): if ret: restart_program.caller(cli, nick, chan, "Updating bot") -@cmd("send", "fsend", flag="F", pm=True) -def fsend(cli, nick, chan, rest): +@command("send", "fsend", flag="F", pm=True) +def fsend(var, wrapper, message): """Forcibly send raw IRC commands to the server.""" - cli.send(rest) + wrapper.source.client.send(message) -def _say(cli, raw_nick, rest, command, action=False): - (nick, _, ident, host) = parse_nick(raw_nick) +def _say(wrapper, rest, cmd, action=False): rest = rest.split(" ", 1) if len(rest) < 2: - pm(cli, nick, messages["fsend_usage"].format( - botconfig.CMD_CHAR, command)) - + wrapper.pm(messages["fsend_usage"].format(botconfig.CMD_CHAR, cmd)) return - (target, message) = rest + target, message = rest - if not is_admin(nick, ident, host): - if not users.exists(nick): - pm(cli, nick, messages["wrong_channel"].format( - botconfig.CHANNEL)) + if target.startswith(tuple(hooks.Features["CHANTYPES"])): + targ = channels.get(target, allow_none=True) + else: + targ = users._get(target, allow_multiple=True) # FIXME + if len(targ) == 1: + targ = targ[0] + else: + targ = None - return - - if rest[0] != botconfig.CHANNEL: - pm(cli, nick, messages["invalid_fsend_permissions"]) + if targ is None: + targ = IRCClient(target, wrapper.source.client) + if not wrapper.source.is_admin(): + if targ is not channels.Main: + wrapper.pm(messages["invalid_fsend_permissions"]) return if action: message = "\u0001ACTION {0}\u0001".format(message) - cli.send("PRIVMSG {0} :{1}".format(target, message)) + targ.send(message, privmsg=True) - -@cmd("say", "fsay", flag="s", raw_nick=True, pm=True) -def fsay(cli, raw_nick, chan, rest): +@command("say", "fsay", flag="s", pm=True) +def fsay(var, wrapper, message): """Talk through the bot as a normal message.""" - _say(cli, raw_nick, rest, "say") + _say(wrapper, message, "say") -@cmd("act", "do", "me", "fact", "fdo", "fme", flag="s", raw_nick=True, pm=True) -def fact(cli, raw_nick, chan, rest): +@command("act", "do", "me", "fact", "fdo", "fme", flag="s", pm=True) +def fact(var, wrapper, message): """Act through the bot as an action.""" - _say(cli, raw_nick, rest, "act", action=True) + _say(wrapper, message, "act", action=True) -def can_run_restricted_cmd(nick): +def can_run_restricted_cmd(user): # if allowed in normal games, restrict it so that it can only be used by dead players and # non-players (don't allow active vengeful ghosts either). # also don't allow in-channel (e.g. make it pm only) @@ -7414,35 +7397,34 @@ def can_run_restricted_cmd(nick): pl = list_participants() - if nick in pl: + if user.nick in pl: # FIXME: Need to update this once list_participants() holds User instances return False - if users.exists(nick) and users.get(nick).account in [users.get(player).account for player in pl if users.exists(player)]: + if user.account in [users._get(player).account for player in pl]: # FIXME return False - hostmask = users.get(nick).ident + "@" + users.get(nick).host - if users.exists(nick) and hostmask in [users.get(player).ident + "@" + users.get(player).host for player in pl if users.exists(player)]: + if user.userhost in [users._get(player).userhost for player in pl]: # FIXME return False return True -@cmd("spectate", "fspectate", flag="A", pm=True, phases=("day", "night")) -def fspectate(cli, nick, chan, rest): +@command("spectate", "fspectate", flag="A", pm=True, phases=("day", "night")) +def fspectate(var, wrapper, message): """Spectate wolfchat or deadchat.""" - if not can_run_restricted_cmd(nick): + if not can_run_restricted_cmd(wrapper.source): pm(cli, nick, messages["fspectate_restricted"]) return - params = rest.split(" ") + params = message.split(" ") on = "on" if not len(params): - pm(cli, nick, messages["fspectate_help"]) + wrapper.pm(messages["fspectate_help"]) return elif len(params) > 1: on = params[1].lower() what = params[0].lower() if what not in ("wolfchat", "deadchat") or on not in ("on", "off"): - pm(cli, nick, messages["fspectate_help"]) + wrapper.pm(messages["fspectate_help"]) return if on == "off": @@ -7450,54 +7432,50 @@ def fspectate(cli, nick, chan, rest): var.SPECTATING_WOLFCHAT.discard(nick) else: var.SPECTATING_DEADCHAT.discard(nick) - pm(cli, nick, messages["fspectate_off"].format(what)) + wrapper.pm(messages["fspectate_off"].format(what)) else: players = [] if what == "wolfchat": var.SPECTATING_WOLFCHAT.add(nick) players = (p for p in list_players() if in_wolflist(p, p)) elif var.ENABLE_DEADCHAT: - if nick in var.DEADCHAT_PLAYERS: - pm(cli, nick, messages["fspectate_in_deadchat"]) + if wrapper.source.nick in var.DEADCHAT_PLAYERS: # FIXME + wrapper.pm(messages["fspectate_in_deadchat"]) return var.SPECTATING_DEADCHAT.add(nick) players = var.DEADCHAT_PLAYERS else: - pm(cli, nick, messages["fspectate_deadchat_disabled"]) + wrapper.pm(messages["fspectate_deadchat_disabled"]) return - pm(cli, nick, messages["fspectate_on"].format(what)) - pm(cli, nick, "People in {0}: {1}".format(what, ", ".join(players))) + wrapper.pm(messages["fspectate_on"].format(what)) + wrapper.pm("People in {0}: {1}".format(what, ", ".join(players))) before_debug_mode_commands = list(COMMANDS.keys()) if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: - @cmd("eval", owner_only=True, pm=True) - def pyeval(cli, nick, chan, rest): + @command("eval", owner_only=True, pm=True) + def pyeval(var, wrapper, message): """Evaluate a Python expression.""" try: - a = str(eval(rest)) - if len(a) < 500: - cli.msg(chan, a) - else: - cli.msg(chan, a[:500]) + wrapper.send(str(eval(message))[:500]) except Exception as e: - cli.msg(chan, "{e.__class__.__name__}: {e}".format(e=e)) + wrapper.send("{e.__class__.__name__}: {e}".format(e=e)) - @cmd("exec", owner_only=True, pm=True) - def py(cli, nick, chan, rest): + @command("exec", owner_only=True, pm=True) + def py(var, wrapper, message): """Execute arbitrary Python code.""" try: - exec(rest) + exec(message) except Exception as e: - cli.msg(chan, "{e.__class__.__name__}: {e}".format(e=e)) + wrapper.send("{e.__class__.__name__}: {e}".format(e=e)) - @cmd("revealroles", flag="a", pm=True, phases=("day", "night")) - def revealroles(cli, nick, chan, rest): + @command("revealroles", flag="a", pm=True, phases=("day", "night")) + def revealroles(var, wrapper, message): """Reveal role information.""" - if not can_run_restricted_cmd(nick): - reply(cli, nick, chan, messages["temp_invalid_perms"], private=True) + if not can_run_restricted_cmd(wrapper.source): + wrapper.pm(messages["temp_invalid_perms"]) return output = [] @@ -7523,7 +7501,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: if var.TURNCOATS[nickname][0] != "none" else "not currently on any side") evt = Event("revealroles_role", {"special_case": special_case}) - evt.dispatch(cli, var, nickname, role) + evt.dispatch(var, wrapper, nickname, role) special_case = evt.data["special_case"] if not evt.prevent_default and nickname not in var.ORIGINAL_ROLES[role] and role not in var.TEMPLATE_RESTRICTIONS: @@ -7568,12 +7546,12 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: output.append("\u0002charmed players\u0002: {0}".format(", ".join(var.CHARMED | var.TOBECHARMED))) evt = Event("revealroles", {"output": output}) - evt.dispatch(cli, var) + evt.dispatch(var, wrapper) if botconfig.DEBUG_MODE: - cli.msg(chan, break_long_message(output, " | ")) + wrapper.send(*output, sep=" | ") else: - reply(cli, nick, chan, break_long_message(output, " | "), private=True) + wrapper.pm(*output, sep=" | ") @cmd("fgame", flag="d", raw_nick=True, phases=("join",)) @@ -7661,7 +7639,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: if fn.chan: fn.caller(cli, user, chan, " ".join(rst)) else: - fn.caller(cli, user, user, " ".join(rst)) + fn.caller(cli, user, users.Bot.nick, " ".join(rst)) cli.msg(chan, messages["operation_successful"]) else: cli.msg(chan, messages["command_not_found"]) @@ -7701,7 +7679,7 @@ if botconfig.DEBUG_MODE or botconfig.ALLOWED_NORMAL_MODE_COMMANDS: if fn.chan: fn.caller(cli, user, chan, " ".join(rst)) else: - fn.caller(cli, user, user, " ".join(rst)) + fn.caller(cli, user, users.Bot.nick, " ".join(rst)) cli.msg(chan, messages["operation_successful"]) else: cli.msg(chan, messages["command_not_found"])